diff --git a/.changeset/analyze-software-creation.md b/.changeset/analyze-software-creation.md deleted file mode 100644 index 312987d3d1..0000000000 --- a/.changeset/analyze-software-creation.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Basic analytics instrumentation is now in place: - -- As users make their way through template steps, a `click` event is fired, including the step number. -- After a user clicks "Create" a `create` event is fired, including the name of the software that was just created. The template used at creation is set on the `entityRef` context key. diff --git a/.changeset/analyze-software-exploration.md b/.changeset/analyze-software-exploration.md deleted file mode 100644 index f535f60127..0000000000 --- a/.changeset/analyze-software-exploration.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Both `EntityProvider` and `AsyncEntityProvider` contexts now wrap all children with an `AnalyticsContext` containing the corresponding `entityRef`; this opens up the possibility for all events underneath these contexts to be associated with and aggregated by the corresponding entity. diff --git a/.changeset/angry-dingos-lick.md b/.changeset/angry-dingos-lick.md new file mode 100644 index 0000000000..933f19a839 --- /dev/null +++ b/.changeset/angry-dingos-lick.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-techdocs-react': patch +--- + +Internal refactor to improve tests diff --git a/.changeset/angry-trees-relax.md b/.changeset/angry-trees-relax.md new file mode 100644 index 0000000000..64dd50366c --- /dev/null +++ b/.changeset/angry-trees-relax.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Enabling the customization of the last step in the scaffolder template. + +To override the content you have to do the next: + +```typescript jsx + +``` diff --git a/.changeset/beige-chairs-repair.md b/.changeset/beige-chairs-repair.md new file mode 100644 index 0000000000..20cad7be6e --- /dev/null +++ b/.changeset/beige-chairs-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +--- + +Refresh (potentially) updated catalog files on `repo:push` more efficiently. diff --git a/.changeset/beige-gorillas-sip.md b/.changeset/beige-gorillas-sip.md deleted file mode 100644 index b0dcaadfc6..0000000000 --- a/.changeset/beige-gorillas-sip.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Fix logic bug that broke techdocs-cli-embedded-app diff --git a/.changeset/big-islands-add.md b/.changeset/big-islands-add.md deleted file mode 100644 index 4a6511562b..0000000000 --- a/.changeset/big-islands-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Set the `searchTooltip` to "Filter" to follow how the `searchPlaceholder` is set making this more consistent diff --git a/.changeset/blue-chefs-know.md b/.changeset/blue-chefs-know.md new file mode 100644 index 0000000000..fa06f52caa --- /dev/null +++ b/.changeset/blue-chefs-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-devops-backend': patch +--- + +Updated installation documentation diff --git a/.changeset/brave-eels-allow.md b/.changeset/brave-eels-allow.md deleted file mode 100644 index 8fbe7ba8d2..0000000000 --- a/.changeset/brave-eels-allow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': minor ---- - -Updated the React Router wiring to make use of the new `basename` property of the router components in React Router v6 stable. To implement this, a new optional `basename` property has been added to the `Router` app component, which can be forwarded to the concrete router implementation in order to support this new behavior. This is done by default in any app that does not have a `Router` component override. diff --git a/.changeset/brown-days-pretend.md b/.changeset/brown-days-pretend.md deleted file mode 100644 index 4b6a328724..0000000000 --- a/.changeset/brown-days-pretend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-tech-insights': patch ---- - -Fixed bug when sending data by Post in `runChecks` and `runBulkChecks` functions of the `TechInsightsClient` class, the default `Content-Type` used was `plain/text` diff --git a/.changeset/calm-bottles-happen.md b/.changeset/calm-bottles-happen.md deleted file mode 100644 index e76db28065..0000000000 --- a/.changeset/calm-bottles-happen.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/backend-common': minor ---- - -**BREAKING CHANGE**: The `UrlReader` interface has been updated to require that `readUrl` is implemented. `readUrl` has previously been optional to implement but a warning has been logged when calling its predecessor `read`. -The `read` method is now deprecated and will be removed in a future release. diff --git a/.changeset/chatty-planets-flash.md b/.changeset/chatty-planets-flash.md deleted file mode 100644 index d6a5c6ad42..0000000000 --- a/.changeset/chatty-planets-flash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-gitlab': patch ---- - -Add `gitlabDiscoveryEntityProviderCatalogModule` (new backend-plugin-api, alpha). diff --git a/.changeset/chilly-ads-lay.md b/.changeset/chilly-ads-lay.md new file mode 100644 index 0000000000..844eaa8dc7 --- /dev/null +++ b/.changeset/chilly-ads-lay.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': minor +--- + +**BREAKING PRODUCERS**: Added a new `getEntitiesByRefs` endpoint to `CatalogApi`, for efficient batch fetching of entities by ref. diff --git a/.changeset/clean-feet-remain.md b/.changeset/clean-feet-remain.md deleted file mode 100644 index c712ef9e1e..0000000000 --- a/.changeset/clean-feet-remain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-github-issues': patch ---- - -Stripping specific issues URL already present to target base issues URL. diff --git a/.changeset/clean-paws-fry.md b/.changeset/clean-paws-fry.md new file mode 100644 index 0000000000..5e66a686c0 --- /dev/null +++ b/.changeset/clean-paws-fry.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': minor +--- + +Introducing repo-tools package diff --git a/.changeset/clean-planets-rhyme.md b/.changeset/clean-planets-rhyme.md deleted file mode 100644 index a6878b491b..0000000000 --- a/.changeset/clean-planets-rhyme.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-azure': patch ---- - -`AzureDevOpsEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. - -Please find how to configure the schedule at the config at -https://backstage.io/docs/integrations/azure/discovery diff --git a/.changeset/clean-seals-whisper.md b/.changeset/clean-seals-whisper.md new file mode 100644 index 0000000000..8891d75f8f --- /dev/null +++ b/.changeset/clean-seals-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': minor +--- + +The `value` of a search analytics event is now set as the total number of search results (when available) diff --git a/.changeset/clever-pillows-drive.md b/.changeset/clever-pillows-drive.md new file mode 100644 index 0000000000..f4e60fb869 --- /dev/null +++ b/.changeset/clever-pillows-drive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Do not use deprecated `LocationSpec` from the `@backstage/plugin-catalog-node` package diff --git a/.changeset/clever-rivers-obey.md b/.changeset/clever-rivers-obey.md new file mode 100644 index 0000000000..0fac8711e9 --- /dev/null +++ b/.changeset/clever-rivers-obey.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Fixes the assignment of group member references in `GithubMultiOrgProcessor` so membership relations are resolved correctly. diff --git a/.changeset/create-app-1669131574.md b/.changeset/create-app-1669131574.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1669131574.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/create-app-1669720399.md b/.changeset/create-app-1669720399.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1669720399.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/dirty-birds-burn.md b/.changeset/dirty-birds-burn.md deleted file mode 100644 index 98f2c5967d..0000000000 --- a/.changeset/dirty-birds-burn.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -`MicrosoftGraphOrgEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. - -Please find how to configure the schedule at the config at -https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-msgraph#readme diff --git a/.changeset/dry-melons-listen.md b/.changeset/dry-melons-listen.md new file mode 100644 index 0000000000..07342fb88e --- /dev/null +++ b/.changeset/dry-melons-listen.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-search-backend': minor +'@backstage/plugin-search-backend-module-elasticsearch': minor +'@backstage/plugin-search-backend-node': minor +--- + +numberOfResults is now provided alongside the query result diff --git a/.changeset/dry-radios-live.md b/.changeset/dry-radios-live.md new file mode 100644 index 0000000000..057d3d0050 --- /dev/null +++ b/.changeset/dry-radios-live.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-explore': patch +--- + +Update search links to only have header as linkable text diff --git a/.changeset/dull-oranges-tap.md b/.changeset/dull-oranges-tap.md deleted file mode 100644 index 946656f610..0000000000 --- a/.changeset/dull-oranges-tap.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@backstage/plugin-techdocs': minor ---- - -Add ability to configure filters when using EntityListDocsGrid - -The following example will render two sections of cards grid: - -- One section for documentations tagged as `recommended` -- One section for documentations tagged as `runbook` - -```js - - entity?.metadata?.tags?.includes('recommended') ?? false, - }, - { - title: "RunBooks Documentation", - filterPredicate: entity => - entity?.metadata?.tags?.includes('runbook') ?? false, - } -]}} /> -``` diff --git a/.changeset/early-hairs-switch.md b/.changeset/early-hairs-switch.md new file mode 100644 index 0000000000..16f4298db5 --- /dev/null +++ b/.changeset/early-hairs-switch.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +changed tests created by create-plugin to follow eslint-rules best practices particularly testing-library/prefer-screen-queries and testing-library/render-result-naming-convention diff --git a/.changeset/early-parrots-guess.md b/.changeset/early-parrots-guess.md new file mode 100644 index 0000000000..4df8dcea95 --- /dev/null +++ b/.changeset/early-parrots-guess.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Initialize all `formData` in the `Stepper` in `/next` diff --git a/.changeset/eight-eels-compete.md b/.changeset/eight-eels-compete.md new file mode 100644 index 0000000000..5cd0cdf041 --- /dev/null +++ b/.changeset/eight-eels-compete.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-playlist-backend': patch +'@backstage/plugin-search-backend-module-pg': patch +'@backstage/plugin-user-settings-backend': patch +--- + +Ensured typescript type checks in migration files. diff --git a/.changeset/eight-pears-attack.md b/.changeset/eight-pears-attack.md deleted file mode 100644 index ca18abb2db..0000000000 --- a/.changeset/eight-pears-attack.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-github': minor ---- - -BREAKING: Support authenticated backends by including a server token for catalog requests. The constructor of `GithubLocationAnalyzer` now requires an instance of `TokenManager` to be supplied: - -```diff -... - builder.addLocationAnalyzers( - new GitHubLocationAnalyzer({ - discovery: env.discovery, - config: env.config, -+ tokenManager: env.tokenManager, - }), - ); -... -``` diff --git a/.changeset/eight-spiders-know.md b/.changeset/eight-spiders-know.md new file mode 100644 index 0000000000..2e3e54dc0f --- /dev/null +++ b/.changeset/eight-spiders-know.md @@ -0,0 +1,15 @@ +--- +'@backstage/backend-common': patch +'@backstage/core-app-api': patch +'@backstage/core-components': patch +'@backstage/version-bridge': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-techdocs': patch +--- + +Internal fixes to avoid implicit usage of globals diff --git a/.changeset/eight-tables-hunt.md b/.changeset/eight-tables-hunt.md new file mode 100644 index 0000000000..bd1be0047f --- /dev/null +++ b/.changeset/eight-tables-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Optimize `Stitcher` process to be more memory efficient diff --git a/.changeset/eleven-pets-sneeze.md b/.changeset/eleven-pets-sneeze.md deleted file mode 100644 index 7c0d8147cc..0000000000 --- a/.changeset/eleven-pets-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-plugin-api': minor ---- - -The app `Router` component now accepts an optional `basename` property. diff --git a/.changeset/empty-taxis-run.md b/.changeset/empty-taxis-run.md new file mode 100644 index 0000000000..d0360b4521 --- /dev/null +++ b/.changeset/empty-taxis-run.md @@ -0,0 +1,22 @@ +--- +'@backstage/backend-app-api': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-node': patch +'@backstage/plugin-events-backend': patch +'@backstage/plugin-events-backend-module-aws-sqs': patch +'@backstage/plugin-events-backend-module-github': patch +'@backstage/plugin-events-backend-module-gitlab': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Refactored to use `coreServices` from `@backstage/backend-plugin-api`. diff --git a/.changeset/fair-walls-talk.md b/.changeset/fair-walls-talk.md new file mode 100644 index 0000000000..3e3ee26160 --- /dev/null +++ b/.changeset/fair-walls-talk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org-react': patch +--- + +Bug fixes and adding the possibility to add a default value for the `GroupListPicker`. Fixes: Vertical size jump on text entry, left align for text, selecting a value closes the popup, auto focus on the popup when opening diff --git a/.changeset/famous-cobras-suffer.md b/.changeset/famous-cobras-suffer.md new file mode 100644 index 0000000000..d09948f876 --- /dev/null +++ b/.changeset/famous-cobras-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Added the githubCredentialsProvider property to the GithubLocationAnalyzerOptions to be able to override the GithubCredentialsProvider. diff --git a/.changeset/fast-lies-remain.md b/.changeset/fast-lies-remain.md new file mode 100644 index 0000000000..74be117920 --- /dev/null +++ b/.changeset/fast-lies-remain.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Added new `POST /entities/by-refs` endpoint, which allows you to efficiently +batch-fetch entities by their entity ref. This can be useful e.g. in graphql +resolvers or similar contexts where you need to fetch many entities at the same +time. diff --git a/.changeset/few-books-remember.md b/.changeset/few-books-remember.md deleted file mode 100644 index 4607202ff6..0000000000 --- a/.changeset/few-books-remember.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-techdocs-node': patch -'@backstage/plugin-techdocs-backend': patch ---- - -Replace usage of deprecataed `UrlReader.read` with `UrlReader.readUrl`. diff --git a/.changeset/fifty-news-stare.md b/.changeset/fifty-news-stare.md new file mode 100644 index 0000000000..24ad2e20e7 --- /dev/null +++ b/.changeset/fifty-news-stare.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-node': minor +--- + +Make it possible for entity providers to supply only entity refs, instead of full entities, in `delta` mutation deletions. diff --git a/.changeset/flat-items-perform.md b/.changeset/flat-items-perform.md deleted file mode 100644 index 4bc846ba7e..0000000000 --- a/.changeset/flat-items-perform.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': minor ---- - -Added a set of default Prometheus metrics around scaffolding. See below for a list of metrics and an explanation of their labels: - -- `scaffolder_task_count`: Tracks successful task runs. - - Labels: - - - `template`: The entity ref of the scaffolded template - - `user`: The entity ref of the user that invoked the template run - - `result`: A string describing whether the task ran successfully, failed, or was skipped - -- `scaffolder_task_duration`: a histogram which tracks the duration of a task run - - Labels: - - - `template`: The entity ref of the scaffolded template - - `result`: A boolean describing whether the task ran successfully - -- `scaffolder_step_count`: a count that tracks each step run - - Labels: - - - `template`: The entity ref of the scaffolded template - - `step`: The name of the step that was run - - `result`: A string describing whether the task ran successfully, failed, or was skipped - -- `scaffolder_step_duration`: a histogram which tracks the duration of each step run - - Labels: - - - `template`: The entity ref of the scaffolded template - - `step`: The name of the step that was run - - `result`: A string describing whether the task ran successfully, failed, or was skipped - -You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/prometheus-metrics.md diff --git a/.changeset/flat-kangaroos-kiss.md b/.changeset/flat-kangaroos-kiss.md deleted file mode 100644 index a79c8621b1..0000000000 --- a/.changeset/flat-kangaroos-kiss.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-gitlab': patch ---- - -`GitlabDiscoveryEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. - -Please find how to configure the schedule at the config at -https://backstage.io/docs/integrations/gitlab/discovery diff --git a/.changeset/fluffy-walls-approve.md b/.changeset/fluffy-walls-approve.md new file mode 100644 index 0000000000..968f5cc402 --- /dev/null +++ b/.changeset/fluffy-walls-approve.md @@ -0,0 +1,21 @@ +--- +'@backstage/plugin-events-backend': minor +--- + +**BREAKING:** Remove required field `router` at `HttpPostIngressEventPublisher.fromConfig` +and replace it with `bind(router: Router)`. +Additionally, the path prefix `/http` will be added inside `HttpPostIngressEventPublisher`. + +```diff +// at packages/backend/src/plugins/events.ts + const eventsRouter = Router(); +- const httpRouter = Router(); +- eventsRouter.use('/http', httpRouter); + + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + logger: env.logger, +- router: httpRouter, + }); ++ http.bind(eventsRouter); +``` diff --git a/.changeset/forty-baboons-burn.md b/.changeset/forty-baboons-burn.md new file mode 100644 index 0000000000..f54b0871c4 --- /dev/null +++ b/.changeset/forty-baboons-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Bump dependency @azure/identity to next minor diff --git a/.changeset/forty-bags-trade.md b/.changeset/forty-bags-trade.md deleted file mode 100644 index 4908134498..0000000000 --- a/.changeset/forty-bags-trade.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Generated development HTTPS backend certificate is now checked for expiration date instead of file age. diff --git a/.changeset/forty-jokes-lie.md b/.changeset/forty-jokes-lie.md deleted file mode 100644 index bd44898004..0000000000 --- a/.changeset/forty-jokes-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-openapi': patch ---- - -Added support to use the `UrlReaders` when `$ref` pointing to a URL. diff --git a/.changeset/four-adults-provide.md b/.changeset/four-adults-provide.md new file mode 100644 index 0000000000..0edb4807d9 --- /dev/null +++ b/.changeset/four-adults-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Provide the ability to change the base currency from USD to any other currency in cost insights plugin diff --git a/.changeset/four-buttons-clean.md b/.changeset/four-buttons-clean.md new file mode 100644 index 0000000000..41af42b45e --- /dev/null +++ b/.changeset/four-buttons-clean.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Handle refresh entity error in `AboutCard`. diff --git a/.changeset/fresh-cooks-sing.md b/.changeset/fresh-cooks-sing.md deleted file mode 100644 index 31f5ff0f22..0000000000 --- a/.changeset/fresh-cooks-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': minor ---- - -This patch brings Gitea as a valid integration: target, via the ScmIntegration interface. It adds gitea to the relevant static properties (get integration by name, get integration by type) for plugins to be able to reference the same Gitea server. diff --git a/.changeset/fresh-news-push.md b/.changeset/fresh-news-push.md new file mode 100644 index 0000000000..a77b7a256b --- /dev/null +++ b/.changeset/fresh-news-push.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-bazaar': patch +--- + +`HomePageBazaarInfoCard` is now displaying `title` instead of `name`. Title is a string that doesn't have to be URL friendly. +The BazaarOverviewCard have the new property `fullHeight`. Link in `BazaarOverviewCard`is moved to header in card. diff --git a/.changeset/fresh-weeks-share.md b/.changeset/fresh-weeks-share.md deleted file mode 100644 index aced6e910c..0000000000 --- a/.changeset/fresh-weeks-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -This patch adds GiteaURLReader to the available classes. It currently only reads single files via gitea's public repos api diff --git a/.changeset/funny-numbers-compete.md b/.changeset/funny-numbers-compete.md new file mode 100644 index 0000000000..604ffe7294 --- /dev/null +++ b/.changeset/funny-numbers-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Use Json types from @backstage/types diff --git a/.changeset/gentle-ligers-help.md b/.changeset/gentle-ligers-help.md new file mode 100644 index 0000000000..f7f9c3b270 --- /dev/null +++ b/.changeset/gentle-ligers-help.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Tweak feature flag registration so that it happens immediately before the first rendering of the app, rather than just after. diff --git a/.changeset/gold-icons-cheat.md b/.changeset/gold-icons-cheat.md new file mode 100644 index 0000000000..3c5e796c05 --- /dev/null +++ b/.changeset/gold-icons-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Bump `msw` version in default plugin/app templates diff --git a/.changeset/gold-yaks-join.md b/.changeset/gold-yaks-join.md new file mode 100644 index 0000000000..29996d1f74 --- /dev/null +++ b/.changeset/gold-yaks-join.md @@ -0,0 +1,6 @@ +--- +'@backstage/test-utils': patch +--- + +The test utility for the plugin context called `MockPluginProvider` has been created. It will be handy in the cases when you use +`__experimentalConfigure` in your plugin. It is experimental and exported through `@backstage/test-utils/alpha`. diff --git a/.changeset/gorgeous-balloons-sit.md b/.changeset/gorgeous-balloons-sit.md deleted file mode 100644 index a83b12131b..0000000000 --- a/.changeset/gorgeous-balloons-sit.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch -'@backstage/plugin-cost-insights': patch -'@backstage/plugin-dynatrace': patch -'@backstage/plugin-stack-overflow-backend': patch -'@backstage/plugin-techdocs': patch ---- - -Tweak README diff --git a/.changeset/gorgeous-hairs-applaud.md b/.changeset/gorgeous-hairs-applaud.md new file mode 100644 index 0000000000..7793c6591e --- /dev/null +++ b/.changeset/gorgeous-hairs-applaud.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Wait for indexer initialization before finalizing indexing. diff --git a/.changeset/gorgeous-onions-thank.md b/.changeset/gorgeous-onions-thank.md deleted file mode 100644 index 8b41bfa5a0..0000000000 --- a/.changeset/gorgeous-onions-thank.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-aws': patch ---- - -`AwsS3EntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. - -Please find how to configure the schedule at the config at -https://backstage.io/docs/integrations/aws-s3/discovery diff --git a/.changeset/gorgeous-queens-pull.md b/.changeset/gorgeous-queens-pull.md deleted file mode 100644 index bb257e85f0..0000000000 --- a/.changeset/gorgeous-queens-pull.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/integration': minor ---- - -Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. - -Deprecates: - -- `getGitHubFileFetchUrl` replaced by `getGithubFileFetchUrl` -- `GitHubIntegrationConfig` replaced by `GithubIntegrationConfig` -- `GitHubIntegration` replaced by `GithubIntegration` -- `readGitHubIntegrationConfig` replaced by `readGithubIntegrationConfig` -- `readGitHubIntegrationConfigs` replaced by `readGithubIntegrationConfigs` -- `replaceGitHubUrlType` replaced by `replaceGithubUrlType` diff --git a/.changeset/great-colts-invite.md b/.changeset/great-colts-invite.md deleted file mode 100644 index 1ca71580dc..0000000000 --- a/.changeset/great-colts-invite.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@backstage/cli': minor ---- - -Added a new ESLint rule that restricts imports of Link from @material-ui - -The rule can be can be overridden in the following way: - -```diff -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { -+ restrictedImports: [ -+ { name: '@material-ui/core', importNames: [] }, -+ { name: '@material-ui/core/Link', importNames: [] }, -+ ], -}); -``` diff --git a/.changeset/grumpy-pigs-reflect.md b/.changeset/grumpy-pigs-reflect.md deleted file mode 100644 index 797aefcba0..0000000000 --- a/.changeset/grumpy-pigs-reflect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-import': patch ---- - -Get rid of `this-is-undefined-in-esm` warning diff --git a/.changeset/happy-avocados-tan.md b/.changeset/happy-avocados-tan.md deleted file mode 100644 index 9f087930d5..0000000000 --- a/.changeset/happy-avocados-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Removed forced capitalization for Entity types in the catalog sidebar. diff --git a/.changeset/healthy-crabs-collect.md b/.changeset/healthy-crabs-collect.md new file mode 100644 index 0000000000..5dc90f9b80 --- /dev/null +++ b/.changeset/healthy-crabs-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kafka-backend': patch +--- + +Internal tweak to adapt to `kafkajs` 2.2.3 diff --git a/.changeset/heavy-elephants-nail.md b/.changeset/heavy-elephants-nail.md deleted file mode 100644 index 473e3eda9e..0000000000 --- a/.changeset/heavy-elephants-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-app-api': minor ---- - -Deprecated the `homepage` config as the component that used it - `HomepageTimer` - has been removed and replaced by the `HeaderWorldClock` in the home plugin diff --git a/.changeset/hip-chairs-tap.md b/.changeset/hip-chairs-tap.md new file mode 100644 index 0000000000..8eca0a9292 --- /dev/null +++ b/.changeset/hip-chairs-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Internal refactor to avoid usage of deprecated symbols diff --git a/.changeset/hip-stingrays-kneel.md b/.changeset/hip-stingrays-kneel.md new file mode 100644 index 0000000000..171e9257f2 --- /dev/null +++ b/.changeset/hip-stingrays-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Create a variable for minimum height and add a prop named 'fit' for determining if the graph height should grow or be contained. diff --git a/.changeset/hip-wasps-yell.md b/.changeset/hip-wasps-yell.md new file mode 100644 index 0000000000..1cdea4e84d --- /dev/null +++ b/.changeset/hip-wasps-yell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-bazaar': patch +--- + +Add `title` as optional parameter to `BazaarOverviewCard` diff --git a/.changeset/itchy-paws-protect.md b/.changeset/itchy-paws-protect.md deleted file mode 100644 index a0c272e5e4..0000000000 --- a/.changeset/itchy-paws-protect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-azure': patch ---- - -Add `azureDevOpsEntityProviderCatalogModule` (new backend-plugin-api, alpha). diff --git a/.changeset/itchy-walls-boil.md b/.changeset/itchy-walls-boil.md new file mode 100644 index 0000000000..2afd0287fe --- /dev/null +++ b/.changeset/itchy-walls-boil.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': minor +--- + +Added the ability to fully customize settings page. Deprecated UserSettingsTab in favour of SettingsLayout.Route diff --git a/.changeset/kind-emus-juggle.md b/.changeset/kind-emus-juggle.md deleted file mode 100644 index 2a99deef81..0000000000 --- a/.changeset/kind-emus-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-aws': patch ---- - -Add `awsS3EntityProviderCatalogModule` (new backend-plugin-api, alpha). diff --git a/.changeset/kind-walls-share.md b/.changeset/kind-walls-share.md new file mode 100644 index 0000000000..5df03999b1 --- /dev/null +++ b/.changeset/kind-walls-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +exported KubernetesContainerRunner, KubernetesContainerRunnerOptions, KubernetesContainerRunnerMountBase diff --git a/.changeset/lazy-planes-repair.md b/.changeset/lazy-planes-repair.md deleted file mode 100644 index 00ecfb095e..0000000000 --- a/.changeset/lazy-planes-repair.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-tasks': patch ---- - -Deprecated the `HumanDuration` type, which should now instead be imported from `@backstage/types`. diff --git a/.changeset/light-snakes-exercise.md b/.changeset/light-snakes-exercise.md new file mode 100644 index 0000000000..45bd37cf27 --- /dev/null +++ b/.changeset/light-snakes-exercise.md @@ -0,0 +1,16 @@ +--- +'@backstage/backend-common': minor +'@backstage/backend-tasks': minor +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-search-backend-node': patch +--- + +Changed to use native `AbortController` and `AbortSignal` from Node.js, instead +of the one from `node-abort-controller`. This is possible now that the minimum +supported Node.js version of the project is 16. + +Note that their interfaces are very slightly different, but typically not in a +way that matters to consumers. If you see any typescript errors as a direct +result from this, they are compatible with each other in the ways that we +interact with them, and should be possible to type-cast across without ill +effects. diff --git a/.changeset/light-turtles-yawn.md b/.changeset/light-turtles-yawn.md new file mode 100644 index 0000000000..3f42fb0524 --- /dev/null +++ b/.changeset/light-turtles-yawn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed deprecated use of `substr` into `substring`. diff --git a/.changeset/little-bikes-eat.md b/.changeset/little-bikes-eat.md deleted file mode 100644 index 0992198146..0000000000 --- a/.changeset/little-bikes-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-tech-radar': patch ---- - -Fixed bug in Tech Radar where, on hover, the tech list quadrant would rerender and scroll top diff --git a/.changeset/loud-rockets-reply.md b/.changeset/loud-rockets-reply.md new file mode 100644 index 0000000000..85ed77f51f --- /dev/null +++ b/.changeset/loud-rockets-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': minor +--- + +Introduces incremental entity providers, which are used for streaming very large data sources into the catalog. diff --git a/.changeset/loud-snails-sleep.md b/.changeset/loud-snails-sleep.md new file mode 100644 index 0000000000..d72c1a22b4 --- /dev/null +++ b/.changeset/loud-snails-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-vault-backend': patch +--- + +Added (optional) config `vault.publicUrl` as alternative to `vault.baseUrl` for `editUrl` and `showUrl` in case `vault.baseUrl` is internal diff --git a/.changeset/lucky-cats-peel.md b/.changeset/lucky-cats-peel.md deleted file mode 100644 index dfe1047b7c..0000000000 --- a/.changeset/lucky-cats-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Small update to fix compatibility with newer versions of the `keyv` library diff --git a/.changeset/lucky-spoons-hide.md b/.changeset/lucky-spoons-hide.md deleted file mode 100644 index c481941f6a..0000000000 --- a/.changeset/lucky-spoons-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Updated `@swc/core` to version 1.3.9 which fixes a `.tsx` parser bug diff --git a/.changeset/many-bikes-press.md b/.changeset/many-bikes-press.md new file mode 100644 index 0000000000..68ddb68c05 --- /dev/null +++ b/.changeset/many-bikes-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING**: All core service references are now exported via a single `coreServices` object. For example, the `loggerServiceRef` is now accessed via `coreServices.logger` instead. diff --git a/.changeset/mean-files-fly.md b/.changeset/mean-files-fly.md deleted file mode 100644 index a7a79bcf74..0000000000 --- a/.changeset/mean-files-fly.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-msgraph': patch ---- - -Add `microsoftGraphOrgEntityProviderCatalogModule` (new backend-plugin-api, alpha). diff --git a/.changeset/metal-dogs-swim.md b/.changeset/metal-dogs-swim.md deleted file mode 100644 index d97c7077f2..0000000000 --- a/.changeset/metal-dogs-swim.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-gerrit': patch ---- - -Add `gerritEntityProviderCatalogModule` (new backend-plugin-api, alpha). diff --git a/.changeset/modern-camels-cheat.md b/.changeset/modern-camels-cheat.md new file mode 100644 index 0000000000..c0a80934d5 --- /dev/null +++ b/.changeset/modern-camels-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Removed `CatalogKindHeader` from `DefaultCatalogPage`. Deprecated `CatalogKindHeader` in favour of `EntityKindPicker`. diff --git a/.changeset/modern-jokes-attack.md b/.changeset/modern-jokes-attack.md new file mode 100644 index 0000000000..3cb3143f05 --- /dev/null +++ b/.changeset/modern-jokes-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Made AlertDisplay not crash on undefined messages diff --git a/.changeset/modern-mugs-shout.md b/.changeset/modern-mugs-shout.md new file mode 100644 index 0000000000..cb845b1fff --- /dev/null +++ b/.changeset/modern-mugs-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add items prop to SupportButton. This prop can be used to override the items that would otherwise be grabbed from the config. diff --git a/.changeset/nasty-crabs-share.md b/.changeset/nasty-crabs-share.md deleted file mode 100644 index d09eb09044..0000000000 --- a/.changeset/nasty-crabs-share.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-github-pull-requests-board': patch ---- - -Add a new "Team" Filter Options to the Github Pull Requests Dashboard. - -When toggling this option on, the dashboard will displays all of the PRs opened -by the members of that team on any repositories of the organization. diff --git a/.changeset/nasty-melons-build.md b/.changeset/nasty-melons-build.md new file mode 100644 index 0000000000..35fb520a5e --- /dev/null +++ b/.changeset/nasty-melons-build.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-events-backend-module-github': patch +--- + +Add `createGithubSignatureValidator(config)` which can be used +to create a validator used at an ingress for topic `github`. + +On top, there is a new `githubWebhookEventsModule` for the new backend plugin API +which auto-registers the `HttpPostIngress` for topic `github` incl. the validator. + +Please find more information at +https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md. diff --git a/.changeset/nervous-mice-flow.md b/.changeset/nervous-mice-flow.md new file mode 100644 index 0000000000..c3a25b9f3d --- /dev/null +++ b/.changeset/nervous-mice-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins-backend': patch +--- + +Remove unnecessary dependency promise-any-polyfill diff --git a/.changeset/new-bugs-march.md b/.changeset/new-bugs-march.md new file mode 100644 index 0000000000..346f5a151e --- /dev/null +++ b/.changeset/new-bugs-march.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-explore': patch +--- + +Adds styling to graph forcing it to always fill out the available space. diff --git a/.changeset/nice-apricots-love.md b/.changeset/nice-apricots-love.md new file mode 100644 index 0000000000..527b644d5f --- /dev/null +++ b/.changeset/nice-apricots-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Prevent duplicate feature flag components from rendering in the settings when using components diff --git a/.changeset/nice-pants-boil.md b/.changeset/nice-pants-boil.md new file mode 100644 index 0000000000..e8bdf0a00f --- /dev/null +++ b/.changeset/nice-pants-boil.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +--- + +Adds MySQL support for the catalog-backend diff --git a/.changeset/nine-ears-whisper.md b/.changeset/nine-ears-whisper.md new file mode 100644 index 0000000000..a1427158af --- /dev/null +++ b/.changeset/nine-ears-whisper.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-events-backend': patch +'@backstage/plugin-events-node': minor +--- + +Introduce a new interface `RequestDetails` to abstract `Request` +providing access to request body and headers. + +**BREAKING:** Replace `request: Request` with `request: RequestDetails` at `RequestValidator`. diff --git a/.changeset/old-bulldogs-fry.md b/.changeset/old-bulldogs-fry.md new file mode 100644 index 0000000000..9c8eef67ad --- /dev/null +++ b/.changeset/old-bulldogs-fry.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Handle GitHub `push` events at the `GithubEntityProvider` by subscribing to the topic `github.push.` + +Implements `EventSubscriber` to receive events for the topic `github.push`. + +On `github.push`, the affected repository will be refreshed. +This includes adding new Location entities, refreshing existing ones, +and removing obsolete ones. + +Please find more information at +https://backstage.io/docs/integrations/github/discovery#installation-with-events-support diff --git a/.changeset/old-keys-leave.md b/.changeset/old-keys-leave.md new file mode 100644 index 0000000000..d80574b2e4 --- /dev/null +++ b/.changeset/old-keys-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added `lifecycleFactory` implementation. diff --git a/.changeset/old-sloths-warn.md b/.changeset/old-sloths-warn.md new file mode 100644 index 0000000000..8eca0a9292 --- /dev/null +++ b/.changeset/old-sloths-warn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Internal refactor to avoid usage of deprecated symbols diff --git a/.changeset/old-tips-cough.md b/.changeset/old-tips-cough.md new file mode 100644 index 0000000000..bcf6de6a53 --- /dev/null +++ b/.changeset/old-tips-cough.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-adr': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-stack-overflow': patch +'@backstage/plugin-techdocs': patch +--- + +Update search links to only have header as linkable text diff --git a/.changeset/orange-trees-peel.md b/.changeset/orange-trees-peel.md deleted file mode 100644 index 344dd88cb9..0000000000 --- a/.changeset/orange-trees-peel.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/catalog-model': patch -'@backstage/core-components': patch -'@backstage/integration': patch -'@backstage/plugin-auth-node': patch -'@backstage/plugin-gcalendar': patch -'@backstage/plugin-periskop': patch -'@backstage/plugin-permission-common': patch -'@backstage/plugin-scaffolder-backend': patch -'@backstage/plugin-techdocs': patch -'@backstage/plugin-xcmetrics': patch ---- - -Internal refactor of imports to avoid circular dependencies diff --git a/.changeset/popular-bulldogs-lie.md b/.changeset/popular-bulldogs-lie.md deleted file mode 100644 index 3bc83338c1..0000000000 --- a/.changeset/popular-bulldogs-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-github': patch ---- - -Properly derive Github credentials when making requests in `GithubLocationAnalyzer` to support Github App authentication diff --git a/.changeset/popular-mails-wave.md b/.changeset/popular-mails-wave.md deleted file mode 100644 index 23517f0608..0000000000 --- a/.changeset/popular-mails-wave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-ilert': minor ---- - -Added support for multiple responders in alert list, added new tab with list to support iLert resource 'service', added new tab with list to support iLert resource 'status page' diff --git a/.changeset/pre.json b/.changeset/pre.json index a2fd9eb458..8c78c0f0b3 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -2,246 +2,274 @@ "mode": "pre", "tag": "next", "initialVersions": { - "example-app": "0.2.76", - "@backstage/app-defaults": "1.0.7", - "example-backend": "0.2.76", - "@backstage/backend-app-api": "0.2.2", - "@backstage/backend-common": "0.15.2", - "@backstage/backend-defaults": "0.1.2", - "example-backend-next": "0.0.4", - "@backstage/backend-plugin-api": "0.1.3", - "@backstage/backend-tasks": "0.3.6", - "@backstage/backend-test-utils": "0.1.29", - "@backstage/catalog-client": "1.1.1", - "@backstage/catalog-model": "1.1.2", - "@backstage/cli": "0.20.0", + "example-app": "0.2.77", + "@backstage/app-defaults": "1.0.8", + "example-backend": "0.2.77", + "@backstage/backend-app-api": "0.2.3", + "@backstage/backend-common": "0.16.0", + "@backstage/backend-defaults": "0.1.3", + "example-backend-next": "0.0.5", + "@backstage/backend-plugin-api": "0.1.4", + "@backstage/backend-tasks": "0.3.7", + "@backstage/backend-test-utils": "0.1.30", + "@backstage/catalog-client": "1.1.2", + "@backstage/catalog-model": "1.1.3", + "@backstage/cli": "0.21.0", "@backstage/cli-common": "0.1.10", - "@backstage/codemods": "0.1.40", - "@backstage/config": "1.0.3", - "@backstage/config-loader": "1.1.5", - "@backstage/core-app-api": "1.1.1", - "@backstage/core-components": "0.11.2", - "@backstage/core-plugin-api": "1.0.7", - "@backstage/create-app": "0.4.32", - "@backstage/dev-utils": "1.0.7", + "@backstage/codemods": "0.1.41", + "@backstage/config": "1.0.4", + "@backstage/config-loader": "1.1.6", + "@backstage/core-app-api": "1.2.0", + "@backstage/core-components": "0.12.0", + "@backstage/core-plugin-api": "1.1.0", + "@backstage/create-app": "0.4.34", + "@backstage/dev-utils": "1.0.8", "e2e-test": "0.2.0", - "@backstage/errors": "1.1.2", - "@backstage/integration": "1.3.2", - "@backstage/integration-react": "1.1.5", - "@backstage/release-manifests": "0.0.6", - "@techdocs/cli": "1.2.2", - "techdocs-cli-embedded-app": "0.2.75", - "@backstage/test-utils": "1.2.1", + "@backstage/errors": "1.1.3", + "@backstage/integration": "1.4.0", + "@backstage/integration-react": "1.1.6", + "@backstage/release-manifests": "0.0.7", + "@backstage/repo-tools": "0.0.0", + "@techdocs/cli": "1.2.3", + "techdocs-cli-embedded-app": "0.2.76", + "@backstage/test-utils": "1.2.2", "@backstage/theme": "0.2.16", - "@backstage/types": "1.0.0", - "@backstage/version-bridge": "1.0.1", - "@backstage/plugin-adr": "0.2.2", - "@backstage/plugin-adr-backend": "0.2.2", - "@backstage/plugin-adr-common": "0.2.2", - "@backstage/plugin-airbrake": "0.3.10", - "@backstage/plugin-airbrake-backend": "0.2.10", - "@backstage/plugin-allure": "0.1.26", - "@backstage/plugin-analytics-module-ga": "0.1.21", - "@backstage/plugin-apache-airflow": "0.2.3", - "@backstage/plugin-api-docs": "0.8.10", + "@backstage/types": "1.0.1", + "@backstage/version-bridge": "1.0.2", + "@backstage/plugin-adr": "0.2.3", + "@backstage/plugin-adr-backend": "0.2.3", + "@backstage/plugin-adr-common": "0.2.3", + "@backstage/plugin-airbrake": "0.3.11", + "@backstage/plugin-airbrake-backend": "0.2.11", + "@backstage/plugin-allure": "0.1.27", + "@backstage/plugin-analytics-module-ga": "0.1.22", + "@backstage/plugin-apache-airflow": "0.2.4", + "@backstage/plugin-api-docs": "0.8.11", "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.0", - "@backstage/plugin-apollo-explorer": "0.1.3", - "@backstage/plugin-app-backend": "0.3.37", - "@backstage/plugin-auth-backend": "0.17.0", - "@backstage/plugin-auth-node": "0.2.6", - "@backstage/plugin-azure-devops": "0.2.1", - "@backstage/plugin-azure-devops-backend": "0.3.16", + "@backstage/plugin-apollo-explorer": "0.1.4", + "@backstage/plugin-app-backend": "0.3.38", + "@backstage/plugin-auth-backend": "0.17.1", + "@backstage/plugin-auth-node": "0.2.7", + "@backstage/plugin-azure-devops": "0.2.2", + "@backstage/plugin-azure-devops-backend": "0.3.17", "@backstage/plugin-azure-devops-common": "0.3.0", - "@backstage/plugin-badges": "0.2.34", - "@backstage/plugin-badges-backend": "0.1.31", - "@backstage/plugin-bazaar": "0.1.25", - "@backstage/plugin-bazaar-backend": "0.2.0", - "@backstage/plugin-bitbucket-cloud-common": "0.2.0", - "@backstage/plugin-bitrise": "0.1.37", - "@backstage/plugin-catalog": "1.6.0", - "@backstage/plugin-catalog-backend": "1.5.0", - "@backstage/plugin-catalog-backend-module-aws": "0.1.10", - "@backstage/plugin-catalog-backend-module-azure": "0.1.8", - "@backstage/plugin-catalog-backend-module-bitbucket": "0.2.4", - "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.4", - "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.2", - "@backstage/plugin-catalog-backend-module-gerrit": "0.1.5", - "@backstage/plugin-catalog-backend-module-github": "0.1.8", - "@backstage/plugin-catalog-backend-module-gitlab": "0.1.8", - "@backstage/plugin-catalog-backend-module-ldap": "0.5.4", - "@backstage/plugin-catalog-backend-module-msgraph": "0.4.3", - "@backstage/plugin-catalog-backend-module-openapi": "0.1.3", - "@backstage/plugin-catalog-common": "1.0.7", - "@internal/plugin-catalog-customized": "0.0.3", - "@backstage/plugin-catalog-graph": "0.2.22", - "@backstage/plugin-catalog-graphql": "0.3.14", - "@backstage/plugin-catalog-import": "0.9.0", - "@backstage/plugin-catalog-node": "1.2.0", - "@backstage/plugin-catalog-react": "1.2.0", - "@backstage/plugin-cicd-statistics": "0.1.12", - "@backstage/plugin-cicd-statistics-module-gitlab": "0.1.6", - "@backstage/plugin-circleci": "0.3.10", - "@backstage/plugin-cloudbuild": "0.3.10", - "@backstage/plugin-code-climate": "0.1.10", - "@backstage/plugin-code-coverage": "0.2.3", - "@backstage/plugin-code-coverage-backend": "0.2.3", - "@backstage/plugin-codescene": "0.1.5", - "@backstage/plugin-config-schema": "0.1.33", - "@backstage/plugin-cost-insights": "0.11.32", + "@backstage/plugin-azure-sites": "0.1.0", + "@backstage/plugin-azure-sites-backend": "0.1.0", + "@backstage/plugin-azure-sites-common": "0.1.0", + "@backstage/plugin-badges": "0.2.35", + "@backstage/plugin-badges-backend": "0.1.32", + "@backstage/plugin-bazaar": "0.2.0", + "@backstage/plugin-bazaar-backend": "0.2.1", + "@backstage/plugin-bitbucket-cloud-common": "0.2.1", + "@backstage/plugin-bitrise": "0.1.38", + "@backstage/plugin-catalog": "1.6.1", + "@backstage/plugin-catalog-backend": "1.5.1", + "@backstage/plugin-catalog-backend-module-aws": "0.1.11", + "@backstage/plugin-catalog-backend-module-azure": "0.1.9", + "@backstage/plugin-catalog-backend-module-bitbucket": "0.2.5", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.1.5", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.3", + "@backstage/plugin-catalog-backend-module-gerrit": "0.1.6", + "@backstage/plugin-catalog-backend-module-github": "0.2.0", + "@backstage/plugin-catalog-backend-module-gitlab": "0.1.9", + "@backstage/plugin-catalog-backend-module-ldap": "0.5.5", + "@backstage/plugin-catalog-backend-module-msgraph": "0.4.4", + "@backstage/plugin-catalog-backend-module-openapi": "0.1.4", + "@backstage/plugin-catalog-common": "1.0.8", + "@internal/plugin-catalog-customized": "0.0.4", + "@backstage/plugin-catalog-graph": "0.2.23", + "@backstage/plugin-catalog-graphql": "0.3.15", + "@backstage/plugin-catalog-import": "0.9.1", + "@backstage/plugin-catalog-node": "1.2.1", + "@backstage/plugin-catalog-react": "1.2.1", + "@backstage/plugin-cicd-statistics": "0.1.13", + "@backstage/plugin-cicd-statistics-module-gitlab": "0.1.7", + "@backstage/plugin-circleci": "0.3.11", + "@backstage/plugin-cloudbuild": "0.3.11", + "@backstage/plugin-code-climate": "0.1.11", + "@backstage/plugin-code-coverage": "0.2.4", + "@backstage/plugin-code-coverage-backend": "0.2.4", + "@backstage/plugin-codescene": "0.1.6", + "@backstage/plugin-config-schema": "0.1.34", + "@backstage/plugin-cost-insights": "0.12.0", "@backstage/plugin-cost-insights-common": "0.1.1", - "@backstage/plugin-dynatrace": "1.0.0", - "@internal/plugin-todo-list": "1.0.6", - "@internal/plugin-todo-list-backend": "1.0.6", - "@internal/plugin-todo-list-common": "1.0.5", - "@backstage/plugin-explore": "0.3.41", - "@backstage/plugin-explore-react": "0.0.22", - "@backstage/plugin-firehydrant": "0.1.27", - "@backstage/plugin-fossa": "0.2.42", - "@backstage/plugin-gcalendar": "0.3.6", - "@backstage/plugin-gcp-projects": "0.3.29", - "@backstage/plugin-git-release-manager": "0.3.23", - "@backstage/plugin-github-actions": "0.5.10", - "@backstage/plugin-github-deployments": "0.1.41", - "@backstage/plugin-github-issues": "0.1.2", - "@backstage/plugin-github-pull-requests-board": "0.1.4", - "@backstage/plugin-gitops-profiles": "0.3.28", - "@backstage/plugin-gocd": "0.1.16", - "@backstage/plugin-graphiql": "0.2.42", - "@backstage/plugin-graphql-backend": "0.1.27", - "@backstage/plugin-home": "0.4.26", - "@backstage/plugin-ilert": "0.1.36", - "@backstage/plugin-jenkins": "0.7.9", - "@backstage/plugin-jenkins-backend": "0.1.27", - "@backstage/plugin-jenkins-common": "0.1.9", - "@backstage/plugin-kafka": "0.3.10", - "@backstage/plugin-kafka-backend": "0.2.30", - "@backstage/plugin-kubernetes": "0.7.3", - "@backstage/plugin-kubernetes-backend": "0.7.3", - "@backstage/plugin-kubernetes-common": "0.4.3", - "@backstage/plugin-lighthouse": "0.3.10", - "@backstage/plugin-newrelic": "0.3.28", - "@backstage/plugin-newrelic-dashboard": "0.2.3", - "@backstage/plugin-org": "0.5.10", - "@backstage/plugin-pagerduty": "0.5.3", - "@backstage/plugin-periskop": "0.1.8", - "@backstage/plugin-periskop-backend": "0.1.8", - "@backstage/plugin-permission-backend": "0.5.12", - "@backstage/plugin-permission-common": "0.7.0", - "@backstage/plugin-permission-node": "0.7.0", - "@backstage/plugin-permission-react": "0.4.6", - "@backstage/plugin-playlist": "0.1.1", - "@backstage/plugin-playlist-backend": "0.2.0", - "@backstage/plugin-playlist-common": "0.1.1", - "@backstage/plugin-proxy-backend": "0.2.31", - "@backstage/plugin-rollbar": "0.4.10", - "@backstage/plugin-rollbar-backend": "0.1.34", - "@backstage/plugin-scaffolder": "1.7.0", - "@backstage/plugin-scaffolder-backend": "1.7.0", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.12", - "@backstage/plugin-scaffolder-backend-module-rails": "0.4.5", - "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.10", - "@backstage/plugin-scaffolder-common": "1.2.1", - "@backstage/plugin-search": "1.0.3", - "@backstage/plugin-search-backend": "1.1.0", - "@backstage/plugin-search-backend-module-elasticsearch": "1.0.3", - "@backstage/plugin-search-backend-module-pg": "0.4.1", - "@backstage/plugin-search-backend-node": "1.0.3", - "@backstage/plugin-search-common": "1.1.0", - "@backstage/plugin-search-react": "1.2.0", - "@backstage/plugin-sentry": "0.4.3", - "@backstage/plugin-shortcuts": "0.3.2", - "@backstage/plugin-sonarqube": "0.4.2", - "@backstage/plugin-sonarqube-backend": "0.1.2", - "@backstage/plugin-splunk-on-call": "0.3.34", - "@backstage/plugin-stack-overflow": "0.1.6", - "@backstage/plugin-stack-overflow-backend": "0.1.6", - "@backstage/plugin-tech-insights": "0.3.1", - "@backstage/plugin-tech-insights-backend": "0.5.3", - "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.21", - "@backstage/plugin-tech-insights-common": "0.2.7", - "@backstage/plugin-tech-insights-node": "0.3.5", - "@backstage/plugin-tech-radar": "0.5.17", - "@backstage/plugin-techdocs": "1.3.3", - "@backstage/plugin-techdocs-addons-test-utils": "1.0.5", - "@backstage/plugin-techdocs-backend": "1.4.0", - "@backstage/plugin-techdocs-module-addons-contrib": "1.0.5", - "@backstage/plugin-techdocs-node": "1.4.1", - "@backstage/plugin-techdocs-react": "1.0.5", - "@backstage/plugin-todo": "0.2.12", - "@backstage/plugin-todo-backend": "0.1.34", - "@backstage/plugin-user-settings": "0.5.0", - "@backstage/plugin-user-settings-backend": "0.1.1", - "@backstage/plugin-vault": "0.1.4", - "@backstage/plugin-vault-backend": "0.2.3", - "@backstage/plugin-xcmetrics": "0.2.30", - "@backstage/plugin-azure-sites": "0.0.0", - "@backstage/plugin-azure-sites-backend": "0.0.0", - "@backstage/plugin-azure-sites-common": "0.0.0" + "@backstage/plugin-dynatrace": "1.0.1", + "@backstage/plugin-events-backend": "0.1.0", + "@backstage/plugin-events-backend-module-aws-sqs": "0.1.0", + "@backstage/plugin-events-backend-module-azure": "0.1.0", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.1.0", + "@backstage/plugin-events-backend-module-gerrit": "0.1.0", + "@backstage/plugin-events-backend-module-github": "0.1.0", + "@backstage/plugin-events-backend-module-gitlab": "0.1.0", + "@backstage/plugin-events-backend-test-utils": "0.1.0", + "@backstage/plugin-events-node": "0.1.0", + "@internal/plugin-todo-list": "1.0.7", + "@internal/plugin-todo-list-backend": "1.0.7", + "@internal/plugin-todo-list-common": "1.0.6", + "@backstage/plugin-explore": "0.3.42", + "@backstage/plugin-explore-react": "0.0.23", + "@backstage/plugin-firehydrant": "0.1.28", + "@backstage/plugin-fossa": "0.2.43", + "@backstage/plugin-gcalendar": "0.3.7", + "@backstage/plugin-gcp-projects": "0.3.30", + "@backstage/plugin-git-release-manager": "0.3.24", + "@backstage/plugin-github-actions": "0.5.11", + "@backstage/plugin-github-deployments": "0.1.42", + "@backstage/plugin-github-issues": "0.2.0", + "@backstage/plugin-github-pull-requests-board": "0.1.5", + "@backstage/plugin-gitops-profiles": "0.3.29", + "@backstage/plugin-gocd": "0.1.17", + "@backstage/plugin-graphiql": "0.2.43", + "@backstage/plugin-graphql-backend": "0.1.28", + "@backstage/plugin-home": "0.4.27", + "@backstage/plugin-ilert": "0.2.0", + "@backstage/plugin-jenkins": "0.7.10", + "@backstage/plugin-jenkins-backend": "0.1.28", + "@backstage/plugin-jenkins-common": "0.1.10", + "@backstage/plugin-kafka": "0.3.11", + "@backstage/plugin-kafka-backend": "0.2.31", + "@backstage/plugin-kubernetes": "0.7.4", + "@backstage/plugin-kubernetes-backend": "0.8.0", + "@backstage/plugin-kubernetes-common": "0.4.4", + "@backstage/plugin-lighthouse": "0.3.11", + "@backstage/plugin-newrelic": "0.3.29", + "@backstage/plugin-newrelic-dashboard": "0.2.4", + "@backstage/plugin-org": "0.6.0", + "@backstage/plugin-org-react": "0.1.0", + "@backstage/plugin-pagerduty": "0.5.4", + "@backstage/plugin-periskop": "0.1.9", + "@backstage/plugin-periskop-backend": "0.1.9", + "@backstage/plugin-permission-backend": "0.5.13", + "@backstage/plugin-permission-common": "0.7.1", + "@backstage/plugin-permission-node": "0.7.1", + "@backstage/plugin-permission-react": "0.4.7", + "@backstage/plugin-playlist": "0.1.2", + "@backstage/plugin-playlist-backend": "0.2.1", + "@backstage/plugin-playlist-common": "0.1.2", + "@backstage/plugin-proxy-backend": "0.2.32", + "@backstage/plugin-rollbar": "0.4.11", + "@backstage/plugin-rollbar-backend": "0.1.35", + "@backstage/plugin-scaffolder": "1.8.0", + "@backstage/plugin-scaffolder-backend": "1.8.0", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.13", + "@backstage/plugin-scaffolder-backend-module-rails": "0.4.6", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.2.11", + "@backstage/plugin-scaffolder-common": "1.2.2", + "@backstage/plugin-search": "1.0.4", + "@backstage/plugin-search-backend": "1.1.1", + "@backstage/plugin-search-backend-module-elasticsearch": "1.0.4", + "@backstage/plugin-search-backend-module-pg": "0.4.2", + "@backstage/plugin-search-backend-node": "1.0.4", + "@backstage/plugin-search-common": "1.1.1", + "@backstage/plugin-search-react": "1.2.1", + "@backstage/plugin-sentry": "0.4.4", + "@backstage/plugin-shortcuts": "0.3.3", + "@backstage/plugin-sonarqube": "0.5.0", + "@backstage/plugin-sonarqube-backend": "0.1.3", + "@backstage/plugin-splunk-on-call": "0.4.0", + "@backstage/plugin-stack-overflow": "0.1.7", + "@backstage/plugin-stack-overflow-backend": "0.1.7", + "@backstage/plugin-tech-insights": "0.3.3", + "@backstage/plugin-tech-insights-backend": "0.5.4", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "0.1.22", + "@backstage/plugin-tech-insights-common": "0.2.8", + "@backstage/plugin-tech-insights-node": "0.3.6", + "@backstage/plugin-tech-radar": "0.5.18", + "@backstage/plugin-techdocs": "1.4.0", + "@backstage/plugin-techdocs-addons-test-utils": "1.0.6", + "@backstage/plugin-techdocs-backend": "1.4.1", + "@backstage/plugin-techdocs-module-addons-contrib": "1.0.6", + "@backstage/plugin-techdocs-node": "1.4.2", + "@backstage/plugin-techdocs-react": "1.0.6", + "@backstage/plugin-todo": "0.2.13", + "@backstage/plugin-todo-backend": "0.1.35", + "@backstage/plugin-user-settings": "0.5.1", + "@backstage/plugin-user-settings-backend": "0.1.2", + "@backstage/plugin-vault": "0.1.5", + "@backstage/plugin-vault-backend": "0.2.4", + "@backstage/plugin-xcmetrics": "0.2.31", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.0.0", + "@backstage/plugin-explore-backend": "0.0.0", + "@backstage/plugin-explore-common": "0.0.0" }, "changesets": [ - "analyze-software-creation", - "analyze-software-exploration", - "big-islands-add", - "brave-eels-allow", - "brown-days-pretend", - "calm-bottles-happen", - "chatty-planets-flash", - "clean-feet-remain", - "clean-planets-rhyme", - "dirty-birds-burn", - "dull-oranges-tap", - "eight-pears-attack", - "eleven-pets-sneeze", - "few-books-remember", - "flat-items-perform", - "flat-kangaroos-kiss", - "forty-bags-trade", - "forty-jokes-lie", - "fresh-cooks-sing", - "fresh-weeks-share", - "gorgeous-balloons-sit", - "gorgeous-onions-thank", - "gorgeous-queens-pull", - "great-colts-invite", - "grumpy-pigs-reflect", - "happy-avocados-tan", - "heavy-elephants-nail", - "itchy-paws-protect", - "kind-emus-juggle", - "lazy-planes-repair", - "little-bikes-eat", - "lucky-cats-peel", - "lucky-spoons-hide", - "mean-files-fly", - "metal-dogs-swim", - "nasty-crabs-share", - "orange-trees-peel", - "popular-bulldogs-lie", - "popular-mails-wave", - "real-swans-repair", - "renovate-6fb5f1b", - "selfish-kiwis-matter", - "shaggy-birds-happen", - "shaggy-colts-watch", - "sharp-goats-itch", - "shiny-beers-relax", - "short-balloons-work", - "sixty-islands-develop", - "sixty-pigs-shave", - "sixty-singers-push", - "spicy-parents-lick", - "spotty-dryers-explain", - "stupid-pens-occur", - "sweet-readers-compare", - "tame-ads-appear", - "tasty-colts-hug", - "tasty-scissors-tickle", - "ten-pens-draw", - "three-houses-agree", - "three-poems-think", - "two-oranges-joke", - "two-yaks-wave", - "unlucky-buttons-poke", - "wet-cameras-call" + "angry-dingos-lick", + "angry-trees-relax", + "chilly-ads-lay", + "clean-paws-fry", + "clever-pillows-drive", + "clever-rivers-obey", + "create-app-1669131574", + "create-app-1669720399", + "early-hairs-switch", + "early-parrots-guess", + "eight-spiders-know", + "eight-tables-hunt", + "fair-walls-talk", + "famous-cobras-suffer", + "fast-lies-remain", + "fluffy-walls-approve", + "forty-baboons-burn", + "four-adults-provide", + "four-buttons-clean", + "fresh-news-push", + "funny-numbers-compete", + "gold-icons-cheat", + "gold-yaks-join", + "gorgeous-hairs-applaud", + "healthy-crabs-collect", + "hip-chairs-tap", + "hip-stingrays-kneel", + "itchy-walls-boil", + "kind-walls-share", + "light-snakes-exercise", + "light-turtles-yawn", + "loud-rockets-reply", + "modern-mugs-shout", + "nasty-melons-build", + "nervous-mice-flow", + "new-bugs-march", + "nine-ears-whisper", + "old-bulldogs-fry", + "old-keys-leave", + "old-tips-cough", + "pretty-eagles-matter", + "pretty-melons-taste", + "quick-horses-cry", + "renovate-3fe8460", + "renovate-4bb70f3", + "renovate-50ee0c3", + "renovate-778b2fa", + "renovate-a5f7839", + "rich-balloons-leave", + "rich-garlics-play", + "rude-pugs-deny", + "rude-sloths-cross", + "search-heavy-frogs-confess", + "search-om-manniskan-ginge", + "search-tofu-panaeng", + "serious-windows-occur", + "silly-wolves-remember", + "slow-dragons-promise", + "slow-eggs-grab", + "slow-flies-marinate", + "slow-flies-marry", + "smart-radios-change", + "soft-nails-arrive", + "sour-flowers-care", + "sour-plums-grow", + "sour-queens-wait", + "stupid-clocks-return", + "stupid-gifts-serve", + "tender-colts-greet", + "tender-parrots-cover", + "thin-donuts-join", + "twelve-meals-smell", + "twenty-dodos-wash", + "warm-parents-notice", + "weak-ears-jam", + "wicked-games-search", + "yellow-forks-knock", + "young-turkeys-relax" ] } diff --git a/.changeset/pretty-eagles-matter.md b/.changeset/pretty-eagles-matter.md new file mode 100644 index 0000000000..64eabd9ce6 --- /dev/null +++ b/.changeset/pretty-eagles-matter.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-bazaar-backend': patch +--- + +Column `title` has replaced column `name` for `BazaarProject` in database diff --git a/.changeset/pretty-melons-taste.md b/.changeset/pretty-melons-taste.md new file mode 100644 index 0000000000..9c2a7a13da --- /dev/null +++ b/.changeset/pretty-melons-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +JSX and React Fast Refresh transforms are no longer enabled when bundling backend code. diff --git a/.changeset/quick-horses-cry.md b/.changeset/quick-horses-cry.md new file mode 100644 index 0000000000..8b40c92b41 --- /dev/null +++ b/.changeset/quick-horses-cry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Update `UserProfileCard` and `GroupProfileCard` to not render links unless the `showLinks` prop is set. The primary component for rendering links are the `EntityLinksCard` from plugin-catalog. diff --git a/.changeset/real-swans-repair.md b/.changeset/real-swans-repair.md deleted file mode 100644 index 67eafc9999..0000000000 --- a/.changeset/real-swans-repair.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-permission-common': patch ---- - -Properly handle rules that have no parameters in `PermissionClient` diff --git a/.changeset/renovate-1e2853d.md b/.changeset/renovate-1e2853d.md new file mode 100644 index 0000000000..9378786e80 --- /dev/null +++ b/.changeset/renovate-1e2853d.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `minimatch` to `5.1.1` and switch version range to `^`. diff --git a/.changeset/renovate-3fe8460.md b/.changeset/renovate-3fe8460.md new file mode 100644 index 0000000000..17dc8b8b97 --- /dev/null +++ b/.changeset/renovate-3fe8460.md @@ -0,0 +1,113 @@ +--- +'@backstage/backend-common': patch +'@backstage/backend-test-utils': patch +'@backstage/catalog-client': patch +'@backstage/cli': patch +'@backstage/config-loader': patch +'@backstage/core-app-api': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/integration-react': patch +'@backstage/integration': patch +'@backstage/release-manifests': patch +'@backstage/test-utils': patch +'@backstage/plugin-adr-backend': patch +'@backstage/plugin-adr': patch +'@backstage/plugin-airbrake-backend': patch +'@backstage/plugin-airbrake': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-apache-airflow': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-apollo-explorer': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-auth-node': patch +'@backstage/plugin-azure-devops-backend': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-azure-sites-backend': patch +'@backstage/plugin-azure-sites': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bitbucket-cloud-common': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-catalog-backend-module-bitbucket': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-graphql': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-code-climate': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-dynatrace': patch +'@backstage/plugin-explore-react': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-firehydrant': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-gcalendar': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-git-release-manager': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-github-issues': patch +'@backstage/plugin-github-pull-requests-board': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-gocd': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-graphql-backend': patch +'@backstage/plugin-home': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-jenkins-backend': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-org-react': patch +'@backstage/plugin-org': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-periskop-backend': patch +'@backstage/plugin-periskop': patch +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-permission-common': patch +'@backstage/plugin-permission-node': patch +'@backstage/plugin-playlist-backend': patch +'@backstage/plugin-playlist': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-rollbar-backend': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube-backend': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-stack-overflow': patch +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-techdocs-addons-test-utils': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-module-addons-contrib': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-todo-backend': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-vault-backend': patch +'@backstage/plugin-vault': patch +'@backstage/plugin-xcmetrics': patch +--- + +Updated dependency `msw` to `^0.49.0`. diff --git a/.changeset/renovate-4bb70f3.md b/.changeset/renovate-4bb70f3.md new file mode 100644 index 0000000000..3a9054011f --- /dev/null +++ b/.changeset/renovate-4bb70f3.md @@ -0,0 +1,17 @@ +--- +'@backstage/core-app-api': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/dev-utils': patch +'@backstage/test-utils': patch +'@backstage/types': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-user-settings': patch +--- + +Updated dependency `zen-observable` to `^0.9.0`. diff --git a/.changeset/renovate-4cbe4f9.md b/.changeset/renovate-4cbe4f9.md new file mode 100644 index 0000000000..90d7faa801 --- /dev/null +++ b/.changeset/renovate-4cbe4f9.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `@rollup/plugin-commonjs` to `^23.0.0`. diff --git a/.changeset/renovate-50ee0c3.md b/.changeset/renovate-50ee0c3.md new file mode 100644 index 0000000000..3cdd142cf6 --- /dev/null +++ b/.changeset/renovate-50ee0c3.md @@ -0,0 +1,17 @@ +--- +'@backstage/core-app-api': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/dev-utils': patch +'@backstage/test-utils': patch +'@backstage/types': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-user-settings': patch +--- + +Updated dependency `zen-observable` to `^0.10.0`. diff --git a/.changeset/renovate-61ad428.md b/.changeset/renovate-61ad428.md new file mode 100644 index 0000000000..95e4e27baf --- /dev/null +++ b/.changeset/renovate-61ad428.md @@ -0,0 +1,8 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-techdocs-module-addons-contrib': patch +--- + +Updated dependency `@react-hookz/web` to `^19.0.0`. diff --git a/.changeset/renovate-6fb5f1b.md b/.changeset/renovate-6fb5f1b.md deleted file mode 100644 index f98cc64ea4..0000000000 --- a/.changeset/renovate-6fb5f1b.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Updated dependency `@svgr/plugin-jsx` to `6.5.x`. -Updated dependency `@svgr/plugin-svgo` to `6.5.x`. -Updated dependency `@svgr/rollup` to `6.5.x`. -Updated dependency `@svgr/webpack` to `6.5.x`. diff --git a/.changeset/renovate-778b2fa.md b/.changeset/renovate-778b2fa.md new file mode 100644 index 0000000000..f4f57e448b --- /dev/null +++ b/.changeset/renovate-778b2fa.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-sonarqube': patch +--- + +Updated dependency `rc-progress` to `3.4.1`. diff --git a/.changeset/renovate-8c69aed.md b/.changeset/renovate-8c69aed.md new file mode 100644 index 0000000000..63a61d941a --- /dev/null +++ b/.changeset/renovate-8c69aed.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Updated dependency `@types/http-errors` to `^2.0.0`. diff --git a/.changeset/renovate-a5f7839.md b/.changeset/renovate-a5f7839.md new file mode 100644 index 0000000000..d1ab22e04a --- /dev/null +++ b/.changeset/renovate-a5f7839.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Updated dependency `typescript-json-schema` to `^0.55.0`. diff --git a/.changeset/renovate-bf8dfb3.md b/.changeset/renovate-bf8dfb3.md new file mode 100644 index 0000000000..57cabf075b --- /dev/null +++ b/.changeset/renovate-bf8dfb3.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `@rollup/plugin-yaml` to `^4.0.0`. diff --git a/.changeset/renovate-ceb9248.md b/.changeset/renovate-ceb9248.md new file mode 100644 index 0000000000..d1cfe1775e --- /dev/null +++ b/.changeset/renovate-ceb9248.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated dependency `@rollup/plugin-json` to `^5.0.0`. diff --git a/.changeset/renovate-ebb2ea1.md b/.changeset/renovate-ebb2ea1.md new file mode 100644 index 0000000000..010236c2f2 --- /dev/null +++ b/.changeset/renovate-ebb2ea1.md @@ -0,0 +1,8 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-techdocs-module-addons-contrib': patch +--- + +Updated dependency `@react-hookz/web` to `^20.0.0`. diff --git a/.changeset/rich-balloons-leave.md b/.changeset/rich-balloons-leave.md new file mode 100644 index 0000000000..bb6832a681 --- /dev/null +++ b/.changeset/rich-balloons-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Added the ability to understand Job Artifact URLs to the GitLab integration diff --git a/.changeset/rich-garlics-play.md b/.changeset/rich-garlics-play.md new file mode 100644 index 0000000000..5589a70e35 --- /dev/null +++ b/.changeset/rich-garlics-play.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-kubernetes-common': patch +--- + +Added Kubernetes proxy API route to backend Kubernetes plugin, allowing Backstage plugin developers to read/write new information from Kubernetes (if proper credentials are provided). diff --git a/.changeset/rude-pugs-deny.md b/.changeset/rude-pugs-deny.md new file mode 100644 index 0000000000..b877d96285 --- /dev/null +++ b/.changeset/rude-pugs-deny.md @@ -0,0 +1,27 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +The catalog no longer stops after the first processor `validateEntityKind` +method returns `true` when validating entity kind shapes. Instead, it continues +through all registered processors that have this method, and requires that _at +least one_ of them returned true. + +The old behavior of stopping early made it harder to extend existing core kinds +with additional fields, since the `BuiltinKindsEntityProcessor` is always +present at the top of the processing chain and ensures that your additional +validation code would never be run. + +This is technically a breaking change, although it should not affect anybody +under normal circumstances, except if you had problematic validation code that +you were unaware that it was not being run. That code may now start to exhibit +those problems. + +If you need to disable this new behavior, `CatalogBuilder` as used in your +`packages/backend/src/plugins/catalog.ts` file now has a +`useLegacySingleProcessorValidation()` method to go back to the old behavior. + +```diff + const builder = await CatalogBuilder.create(env); ++builder.useLegacySingleProcessorValidation(); +``` diff --git a/.changeset/rude-sloths-cross.md b/.changeset/rude-sloths-cross.md new file mode 100644 index 0000000000..8d3fc486e8 --- /dev/null +++ b/.changeset/rude-sloths-cross.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-explore': patch +'@backstage/plugin-explore-backend': patch +'@backstage/plugin-explore-common': patch +'@backstage/plugin-explore-react': patch +--- + +Added new `@backstage/plugin-explore-backend` & `@backstage/plugin-explore-common` packages. + +This deprecates the `ExploreToolsConfig` API (in `@backstage/plugin-explore-react`) which is replaced by the `ExploreApi` & `ExploreClient`. The list of `ExploreTool` data can now be provided on the backend by either using the supplied `StaticExploreToolProvider` or by implementing a custom `ExploreToolProvider`. See the [explore-backend README](https://github.com/backstage/backstage/blob/master/plugins/explore-backend/README.md) for full details. + +NOTE: Existing installations that have customized the `ExploreToolConfig` will continue to work through the new `ExploreClient`. However, existing data should be migrated over to new `explore-backend` plugin as `ExploreToolConfig` will be removed in the future. + +BREAKING CHANGE: If you have previously installed the `explore` plugin, but not yet customized the `ExploreToolConfig` API in your `packages/app`, this will result in an empty list of tools. diff --git a/.changeset/search-heavy-frogs-confess.md b/.changeset/search-heavy-frogs-confess.md new file mode 100644 index 0000000000..a37c28c60c --- /dev/null +++ b/.changeset/search-heavy-frogs-confess.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Fixed a bug that could cause a `max listeners exceeded warning` to be logged when more than 10 collators were running simultaneously. diff --git a/.changeset/search-lieutenant-dangle.md b/.changeset/search-lieutenant-dangle.md new file mode 100644 index 0000000000..75f1bca915 --- /dev/null +++ b/.changeset/search-lieutenant-dangle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Fixed a bug that prevented indices from being cleaned up under some circumstances, which could have led to shard exhaustion. diff --git a/.changeset/search-om-manniskan-ginge.md b/.changeset/search-om-manniskan-ginge.md new file mode 100644 index 0000000000..c36972f191 --- /dev/null +++ b/.changeset/search-om-manniskan-ginge.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Updated indexer and decorator base classes to take advantage of features introduced in Node.js v16; be sure you are running a [supported version of Node.js](https://backstage.io/docs/releases/v1.8.0#node-16-and-18). diff --git a/.changeset/search-tofu-panaeng.md b/.changeset/search-tofu-panaeng.md new file mode 100644 index 0000000000..1f8bbf6a7b --- /dev/null +++ b/.changeset/search-tofu-panaeng.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-node': patch +--- + +Use of `TestPipeline.withSubject()` is now deprecated. Instead, use the `fromCollator`, `fromDecorator`, or `fromIndexer` static methods to instantiate a test pipeline. You may also use the class' `withCollator`, `withDecorator`, and `withIndexer` instance methods to build test pipelines that consist of multiple test subjects. diff --git a/.changeset/selfish-kiwis-matter.md b/.changeset/selfish-kiwis-matter.md deleted file mode 100644 index e258af0e97..0000000000 --- a/.changeset/selfish-kiwis-matter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Updated `@swc/core` to `v1.3.9` which fixes a `.tsx` parser bug. You may want to run `yarn backstage-cli versions:bump` to get on latest version including the CLI itself. diff --git a/.changeset/selfish-lizards-invent.md b/.changeset/selfish-lizards-invent.md new file mode 100644 index 0000000000..fd486416d8 --- /dev/null +++ b/.changeset/selfish-lizards-invent.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-analytics-module-ga': patch +--- + +Added support for sending virtual pageviews on `search` events in order to enable +Site Search functionality in GA. For more information consult [README](/plugins/analytics-module-ga/README.md#enabling-site-search) diff --git a/.changeset/serious-windows-occur.md b/.changeset/serious-windows-occur.md new file mode 100644 index 0000000000..37f778e846 --- /dev/null +++ b/.changeset/serious-windows-occur.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': patch +'@backstage/core-components': patch +--- + +Internal refactor to avoid usage of deprecated symbols. diff --git a/.changeset/shaggy-birds-happen.md b/.changeset/shaggy-birds-happen.md deleted file mode 100644 index 7f7a691c85..0000000000 --- a/.changeset/shaggy-birds-happen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-github-issues': minor ---- - -**BREAKING**: Changed the casing of all exported types to have a lowercase "h" in "github". E.g. "GitHubIssuesPage" was renamed to "GithubIssuesPage". Please rename your imports where necessary. diff --git a/.changeset/shaggy-colts-watch.md b/.changeset/shaggy-colts-watch.md deleted file mode 100644 index 67003bd2d3..0000000000 --- a/.changeset/shaggy-colts-watch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Disable base path workaround in `Link` component when React Router v6 stable is used. diff --git a/.changeset/sharp-goats-itch.md b/.changeset/sharp-goats-itch.md deleted file mode 100644 index 601d6a7e9d..0000000000 --- a/.changeset/sharp-goats-itch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/types': patch ---- - -Added the `HumanDuration` type, moved here from `@backstage/backend-tasks`. This type matches the `Duration.fromObject` form of `luxon`. diff --git a/.changeset/sharp-shoes-bathe.md b/.changeset/sharp-shoes-bathe.md new file mode 100644 index 0000000000..8d3c0dfddb --- /dev/null +++ b/.changeset/sharp-shoes-bathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +--- + +Make incremental providers more resilient to failures diff --git a/.changeset/shiny-beers-relax.md b/.changeset/shiny-beers-relax.md deleted file mode 100644 index be8e9b6fb7..0000000000 --- a/.changeset/shiny-beers-relax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Updated the create-app command to no longer require Git to be installed and configured. A git repository will only be initialized if possible and if not already in an git repository. diff --git a/.changeset/short-balloons-work.md b/.changeset/short-balloons-work.md deleted file mode 100644 index 5d0f66851d..0000000000 --- a/.changeset/short-balloons-work.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@backstage/plugin-bitrise': patch -'@backstage/plugin-code-coverage': patch -'@backstage/plugin-cost-insights': minor -'@backstage/plugin-git-release-manager': patch -'@backstage/plugin-xcmetrics': patch ---- - -Updated recharts to v2.0.0 and fixed typing issues diff --git a/.changeset/short-turtles-dream.md b/.changeset/short-turtles-dream.md new file mode 100644 index 0000000000..022ef35f58 --- /dev/null +++ b/.changeset/short-turtles-dream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-stack-overflow-backend': patch +--- + +Added option to supply API Access Token. This is required in addition to an API key when trying to access the data for a private Stack Overflow Team. diff --git a/.changeset/shy-cycles-add.md b/.changeset/shy-cycles-add.md new file mode 100644 index 0000000000..2c70f2a8eb --- /dev/null +++ b/.changeset/shy-cycles-add.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Set the default `maxDepth` prop for `EntityRelationsGraph` to a smaller value to provide better readability. diff --git a/.changeset/silly-wolves-remember.md b/.changeset/silly-wolves-remember.md new file mode 100644 index 0000000000..f39812db88 --- /dev/null +++ b/.changeset/silly-wolves-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Registered shutdown hook in experimental catalog plugin. diff --git a/.changeset/silver-rats-rest.md b/.changeset/silver-rats-rest.md new file mode 100644 index 0000000000..52bb9acd6f --- /dev/null +++ b/.changeset/silver-rats-rest.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-common': minor +--- + +numberOfResults (total number of results for a given query) can now be provided by each search engine and consumed as part of the search results response diff --git a/.changeset/sixty-islands-develop.md b/.changeset/sixty-islands-develop.md deleted file mode 100644 index 9eb36cc1a9..0000000000 --- a/.changeset/sixty-islands-develop.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder': minor ---- - -Added props to override default Scaffolder page title, subtitle and pageTitleOverride. -Routes like `rootRouteRef`, `selectedTemplateRouteRef`, `nextRouteRef`, `nextSelectedTemplateRouteRef` were made public and can be used in your app (e.g. in custom TemplateCard component). diff --git a/.changeset/sixty-pigs-shave.md b/.changeset/sixty-pigs-shave.md deleted file mode 100644 index e1f4443ad4..0000000000 --- a/.changeset/sixty-pigs-shave.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/plugin-catalog-backend-module-github': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-git-release-manager': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-github-issues': patch -'@backstage/plugin-github-pull-requests-board': patch -'@backstage/plugin-techdocs': patch -'@backstage/plugin-techdocs-module-addons-contrib': patch ---- - -Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. diff --git a/.changeset/sixty-singers-push.md b/.changeset/sixty-singers-push.md deleted file mode 100644 index b5b50ae077..0000000000 --- a/.changeset/sixty-singers-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-bazaar': minor ---- - -The limit prop of BazaarOverviewCard has been removed entirely, and instead replaced with a new optional boolean prop `fullWidth`. The BazaarOverviewCard now always use full height without fixed width. Also fixed problem with link to Bazaar. diff --git a/.changeset/slow-dragons-promise.md b/.changeset/slow-dragons-promise.md new file mode 100644 index 0000000000..045b4f34e3 --- /dev/null +++ b/.changeset/slow-dragons-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Fix incorrectly exported GithubOrgEntityProvider as a type diff --git a/.changeset/slow-eggs-grab.md b/.changeset/slow-eggs-grab.md new file mode 100644 index 0000000000..e4c537687f --- /dev/null +++ b/.changeset/slow-eggs-grab.md @@ -0,0 +1,8 @@ +--- +'@backstage/repo-tools': minor +--- + +Api reference documentation improvements + +- breadcrumbs links semantics as code spans +- new `@config` annotation to describe related config keys diff --git a/.changeset/slow-flies-marinate.md b/.changeset/slow-flies-marinate.md new file mode 100644 index 0000000000..42d0de5f44 --- /dev/null +++ b/.changeset/slow-flies-marinate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-events-backend-module-aws-sqs': patch +--- + +Upgrade to AWS SDK for Javascript v3 diff --git a/.changeset/slow-flies-marry.md b/.changeset/slow-flies-marry.md new file mode 100644 index 0000000000..7882707d32 --- /dev/null +++ b/.changeset/slow-flies-marry.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Upgrade to AWS SDK for Javascript v3 + +Techdocs support for AWS S3 now requires defining the AWS region to connect to. +If `techdocs.publisher.awsS3.region` is missing from the config, the AWS environment variable `AWS_REGION` will be used. diff --git a/.changeset/slow-lies-jam.md b/.changeset/slow-lies-jam.md new file mode 100644 index 0000000000..a9d7a90b95 --- /dev/null +++ b/.changeset/slow-lies-jam.md @@ -0,0 +1,11 @@ +--- +'@backstage/backend-common': patch +'@backstage/cli': patch +'@backstage/cli-common': patch +'@backstage/core-app-api': patch +'@backstage/core-components': patch +'@backstage/dev-utils': patch +'@techdocs/cli': patch +--- + +Tweaked wording to use inclusive terminology diff --git a/.changeset/smart-radios-change.md b/.changeset/smart-radios-change.md new file mode 100644 index 0000000000..1c7393829d --- /dev/null +++ b/.changeset/smart-radios-change.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Fixed `SingleHostDiscovery` so that it properly handles single-string `backend.listen` configurations such as `:80`. diff --git a/.changeset/soft-nails-arrive.md b/.changeset/soft-nails-arrive.md new file mode 100644 index 0000000000..e9c0a40a05 --- /dev/null +++ b/.changeset/soft-nails-arrive.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Implement Custom Field Explorer to view and play around with available installed custom field extensions diff --git a/.changeset/sour-flowers-care.md b/.changeset/sour-flowers-care.md new file mode 100644 index 0000000000..8524c4981c --- /dev/null +++ b/.changeset/sour-flowers-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Adds the ability to supply a `transformErrors` function to the `Stepper` for `/next` diff --git a/.changeset/sour-plums-grow.md b/.changeset/sour-plums-grow.md new file mode 100644 index 0000000000..4070dc5e50 --- /dev/null +++ b/.changeset/sour-plums-grow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +fixed `headerOptions` not passed to `TemplatePage` component diff --git a/.changeset/sour-queens-wait.md b/.changeset/sour-queens-wait.md new file mode 100644 index 0000000000..87231a1eb0 --- /dev/null +++ b/.changeset/sour-queens-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +add the command type-deps to the repo tool package. diff --git a/.changeset/spicy-parents-lick.md b/.changeset/spicy-parents-lick.md deleted file mode 100644 index 692745ed12..0000000000 --- a/.changeset/spicy-parents-lick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Include query parameters when navigating to relative links in documents diff --git a/.changeset/spotty-dryers-explain.md b/.changeset/spotty-dryers-explain.md deleted file mode 100644 index 41bef3fbf7..0000000000 --- a/.changeset/spotty-dryers-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-xcmetrics': patch ---- - -Removed an unused and hidden build details route. diff --git a/.changeset/strange-rocks-hide.md b/.changeset/strange-rocks-hide.md new file mode 100644 index 0000000000..e171a277fb --- /dev/null +++ b/.changeset/strange-rocks-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +The search table also holds the original entity value now and the facets endpoint fetches the filtered entity data from the search table. diff --git a/.changeset/strong-peaches-melt.md b/.changeset/strong-peaches-melt.md new file mode 100644 index 0000000000..43f819fd4d --- /dev/null +++ b/.changeset/strong-peaches-melt.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/test-utils': patch +--- + +Sync components in @backstage/core-components with the Component Design Guidelines diff --git a/.changeset/stupid-clocks-return.md b/.changeset/stupid-clocks-return.md new file mode 100644 index 0000000000..84b061334f --- /dev/null +++ b/.changeset/stupid-clocks-return.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-events-backend-module-gitlab': patch +--- + +Add `createGitlabTokenValidator(config)` which can be used +to create a validator used at an ingress for topic `gitlab`. + +On top, there is a new `gitlabWebhookEventsModule` for the new backend plugin API +which auto-registers the `HttpPostIngress` for topic `gitlab` incl. the validator. + +Please find more information at +https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-gitlab/README.md. diff --git a/.changeset/stupid-gifts-serve.md b/.changeset/stupid-gifts-serve.md new file mode 100644 index 0000000000..3110a1976c --- /dev/null +++ b/.changeset/stupid-gifts-serve.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-bitbucket-cloud': patch +--- + +Fix repo:push topic not matching `BitbucketCloudEventRouter`. diff --git a/.changeset/stupid-pens-occur.md b/.changeset/stupid-pens-occur.md deleted file mode 100644 index d8af9a447a..0000000000 --- a/.changeset/stupid-pens-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': minor ---- - -**BREAKING**: Removed the `HomepageTimer` as it has been replaced by the `HeaderWorldClock` in the Home plugin and was deprecated over a year ago. diff --git a/.changeset/sweet-readers-compare.md b/.changeset/sweet-readers-compare.md deleted file mode 100644 index dd61b0afdc..0000000000 --- a/.changeset/sweet-readers-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-circleci': patch ---- - -Update screenshots in documentation to match latest CircleCI plugin diff --git a/.changeset/tame-ads-appear.md b/.changeset/tame-ads-appear.md deleted file mode 100644 index 887cc73252..0000000000 --- a/.changeset/tame-ads-appear.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-tech-insights-backend': patch -'@backstage/plugin-tech-insights-node': patch ---- - -Add a default delay to the fact retrievers to prevent cold-start errors diff --git a/.changeset/tasty-colts-hug.md b/.changeset/tasty-colts-hug.md deleted file mode 100644 index e56d48a441..0000000000 --- a/.changeset/tasty-colts-hug.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-azure-sites': minor -'@backstage/plugin-azure-sites-backend': minor -'@backstage/plugin-azure-sites-common': minor ---- - -Azure Sites (Apps & Functions) support for a given entity. View the current status of the site, quickly jump to site's Overview page, or Log Stream page. diff --git a/.changeset/tasty-scissors-tickle.md b/.changeset/tasty-scissors-tickle.md deleted file mode 100644 index 745172df36..0000000000 --- a/.changeset/tasty-scissors-tickle.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-bitbucket-server': patch ---- - -`BitbucketServerEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. - -Please find how to configure the schedule at the config at -https://backstage.io/docs/integrations/bitbucketServer/discovery diff --git a/.changeset/ten-pens-draw.md b/.changeset/ten-pens-draw.md deleted file mode 100644 index be35a6f3b0..0000000000 --- a/.changeset/ten-pens-draw.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-gerrit': patch ---- - -`GerritEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. - -Please find how to configure the schedule at the config at -https://backstage.io/docs/integrations/gerrit/discovery diff --git a/.changeset/tender-colts-greet.md b/.changeset/tender-colts-greet.md new file mode 100644 index 0000000000..ff59bf0bea --- /dev/null +++ b/.changeset/tender-colts-greet.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-openapi': patch +--- + +Enabled support of resolving `$refs` in all kind of yaml documents, not only OpenAPI. This implicitly adds `$ref` resolving support for AsyncAPI specs. Thus, the `openApiPlaceholderResolver` has been renamed to `jsonSchemaRefPlaceholderResolver`. diff --git a/.changeset/tender-parrots-cover.md b/.changeset/tender-parrots-cover.md new file mode 100644 index 0000000000..5272fcbdce --- /dev/null +++ b/.changeset/tender-parrots-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-module-addons-contrib': patch +--- + +Use `app.title` from `app-config.yaml` when creating new Documentation Feedback issue. `Backstage` is the default value. diff --git a/.changeset/thin-donuts-join.md b/.changeset/thin-donuts-join.md new file mode 100644 index 0000000000..84705711d2 --- /dev/null +++ b/.changeset/thin-donuts-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fixed Entity kind pluralisation in the `CatalogKindHeader` component. diff --git a/.changeset/thin-flies-wink.md b/.changeset/thin-flies-wink.md new file mode 100644 index 0000000000..a31db67a1c --- /dev/null +++ b/.changeset/thin-flies-wink.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Bump `json-schema-library` to version `^7.3.9` which does not pull in the `gson-pointer` library diff --git a/.changeset/three-houses-agree.md b/.changeset/three-houses-agree.md deleted file mode 100644 index 041cd471d3..0000000000 --- a/.changeset/three-houses-agree.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-tech-insights-backend': patch -'@backstage/plugin-tech-insights-node': patch ---- - -Use `HumanDuration` from `@backstage/types` diff --git a/.changeset/three-poems-think.md b/.changeset/three-poems-think.md deleted file mode 100644 index aad3cae117..0000000000 --- a/.changeset/three-poems-think.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-github': minor ---- - -Added the ability for the GitHub discovery provider to validate that catalog files exist before emitting them. - -Users can now set the `validateLocationsExist` property to `true` in their GitHub discovery configuration to opt in to this feature. -This feature only works with `catalogPath`s that do not contain wildcards. - -When `validateLocationsExist` is set to `true`, the GitHub discovery provider will retrieve the object from the -repository at the provided `catalogPath`. -If this file exists and is non-empty, then it will be emitted as a location for further processing. -If this file does not exist or is empty, then it will not be emitted. -Not emitting locations that do not exist allows for far fewer calls to the GitHub API to validate locations that do not exist. diff --git a/.changeset/twelve-meals-smell.md b/.changeset/twelve-meals-smell.md new file mode 100644 index 0000000000..a9d64823a3 --- /dev/null +++ b/.changeset/twelve-meals-smell.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-vault-backend': patch +--- + +Use `express-promise-router` to catch errors properly. +Add `403` error as a known one. It will now return a `NotAllowed` error. diff --git a/.changeset/twenty-dodos-wash.md b/.changeset/twenty-dodos-wash.md new file mode 100644 index 0000000000..d21088a953 --- /dev/null +++ b/.changeset/twenty-dodos-wash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Added `lifecycleFactory` to default service factories. diff --git a/.changeset/two-oranges-joke.md b/.changeset/two-oranges-joke.md deleted file mode 100644 index 7f116e8ab0..0000000000 --- a/.changeset/two-oranges-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-bitbucket-server': patch ---- - -Add `bitbucketServerEntityProviderCatalogModule` (new backend-plugin-api, alpha). diff --git a/.changeset/two-yaks-wave.md b/.changeset/two-yaks-wave.md deleted file mode 100644 index 97c2908928..0000000000 --- a/.changeset/two-yaks-wave.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@backstage/core-components': patch -'@backstage/create-app': patch -'@backstage/plugin-azure-sites': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-circleci': patch -'@backstage/plugin-cloudbuild': patch -'@backstage/plugin-dynatrace': patch -'@backstage/plugin-gcalendar': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-github-issues': patch -'@backstage/plugin-gitops-profiles': patch -'@backstage/plugin-jenkins': patch -'@backstage/plugin-kafka': patch -'@backstage/plugin-pagerduty': patch -'@backstage/plugin-rollbar': patch -'@backstage/plugin-scaffolder': patch ---- - -Prefer using `Link` from `@backstage/core-components` rather than material-UI. diff --git a/.changeset/unlucky-buttons-poke.md b/.changeset/unlucky-buttons-poke.md deleted file mode 100644 index b0ccd2039a..0000000000 --- a/.changeset/unlucky-buttons-poke.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@backstage/integration': patch -'@backstage/plugin-catalog-backend-module-aws': patch -'@backstage/plugin-catalog-backend-module-gerrit': patch -'@backstage/plugin-stack-overflow-backend': patch -'@backstage/plugin-techdocs-backend': patch ---- - -Remove explicit default visibility at `config.d.ts` files. - -```ts -/** - * @visibility backend - */ -``` diff --git a/.changeset/warm-parents-notice.md b/.changeset/warm-parents-notice.md new file mode 100644 index 0000000000..0084a968a1 --- /dev/null +++ b/.changeset/warm-parents-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-proxy-backend': patch +--- + +Documented the `createRouter` method. diff --git a/.changeset/weak-ears-jam.md b/.changeset/weak-ears-jam.md new file mode 100644 index 0000000000..8f20937998 --- /dev/null +++ b/.changeset/weak-ears-jam.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Added `integrations.github.apps.allowedInstallationOwners` to the configuration schema. diff --git a/.changeset/wet-cameras-call.md b/.changeset/wet-cameras-call.md deleted file mode 100644 index 3d58719269..0000000000 --- a/.changeset/wet-cameras-call.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@backstage/plugin-kubernetes-backend': minor -'@backstage/plugin-kubernetes-common': patch ---- - -The Kubernetes errors when fetching pod metrics are now captured and returned to the frontend. - -- **BREAKING** The method `fetchPodMetricsByNamespace` in the interface `KubernetesFetcher` is changed to `fetchPodMetricsByNamespaces`. It now accepts a set of namespace strings and returns `Promise`. -- Add the `PodStatusFetchResponse` to the `FetchResponse` union type. -- Add `NOT_FOUND` to the `KubernetesErrorTypes` union type, the HTTP error with status code 404 will be mapped to this error. diff --git a/.changeset/wicked-games-search.md b/.changeset/wicked-games-search.md new file mode 100644 index 0000000000..5264995a2d --- /dev/null +++ b/.changeset/wicked-games-search.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +added an optional config entry `costInsights.engineerThreshold` to allow users to control the threshold value for the 'negligible' change in costs. diff --git a/.changeset/yellow-forks-knock.md b/.changeset/yellow-forks-knock.md new file mode 100644 index 0000000000..98c4b5b675 --- /dev/null +++ b/.changeset/yellow-forks-knock.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Making a possibility to hide a trending line in a cost insights plugin diff --git a/.changeset/young-turkeys-relax.md b/.changeset/young-turkeys-relax.md new file mode 100644 index 0000000000..c53cd984a6 --- /dev/null +++ b/.changeset/young-turkeys-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added initial support for registering shutdown hooks via `lifecycleServiceRef`. diff --git a/.eslintrc.js b/.eslintrc.js index 47a47daee6..675a834efb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,7 +18,7 @@ var path = require('path'); module.exports = { root: true, - plugins: ['notice'], + plugins: ['notice', 'testing-library'], rules: { 'notice/notice': [ 'error', @@ -45,5 +45,196 @@ module.exports = { "CallExpression[arguments.length=0] > MemberExpression[property.name='toUpperCase']", }, ], + 'testing-library/await-async-query': 'error', + 'testing-library/await-async-utils': 'error', + 'testing-library/no-await-sync-query': 'error', + 'testing-library/prefer-wait-for': 'error', + 'testing-library/no-dom-import': 'error', + 'testing-library/no-wait-for-side-effects': 'error', + 'testing-library/no-wait-for-empty-callback': 'error', + 'no-restricted-globals': [ + 'error', + [ + 'blur', + 'captureEvents', + 'chrome', + 'clientInformation', + 'close', + 'closed', + 'createImageBitmap', + 'crypto', + 'customElements', + 'defaultstatus', + 'defaultStatus', + 'devicePixelRatio', + 'external', + 'find', + 'focus', + 'frameElement', + 'frames', + 'getComputedStyle', + 'getSelection', + 'indexedDB', + 'innerHeight', + 'innerWidth', + 'isSecureContext', + 'length', + 'location', + 'locationbar', + 'matchMedia', + 'menubar', + 'moveBy', + 'moveTo', + 'name', + 'navigator', + 'onabort', + 'onafterprint', + 'onanimationend', + 'onanimationiteration', + 'onanimationstart', + 'onappinstalled', + 'onauxclick', + 'onbeforeinstallprompt', + 'onbeforeprint', + 'onbeforeunload', + 'onblur', + 'oncancel', + 'oncanplay', + 'oncanplaythrough', + 'onchange', + 'onclick', + 'onclose', + 'oncontextmenu', + 'oncuechange', + 'ondblclick', + 'ondevicemotion', + 'ondeviceorientation', + 'ondeviceorientationabsolute', + 'ondrag', + 'ondragend', + 'ondragenter', + 'ondragleave', + 'ondragover', + 'ondragstart', + 'ondrop', + 'ondurationchange', + 'onemptied', + 'onended', + 'onerror', + 'onfocus', + 'ongotpointercapture', + 'onhashchange', + 'oninput', + 'oninvalid', + 'onkeydown', + 'onkeypress', + 'onkeyup', + 'onlanguagechange', + 'onload', + 'onloadeddata', + 'onloadedmetadata', + 'onloadstart', + 'onlostpointercapture', + 'onmessage', + 'onmessageerror', + 'onmousedown', + 'onmouseenter', + 'onmouseleave', + 'onmousemove', + 'onmouseout', + 'onmouseover', + 'onmouseup', + 'onmousewheel', + 'onoffline', + 'ononline', + 'onpagehide', + 'onpageshow', + 'onpause', + 'onplay', + 'onplaying', + 'onpointercancel', + 'onpointerdown', + 'onpointerenter', + 'onpointerleave', + 'onpointermove', + 'onpointerout', + 'onpointerover', + 'onpointerup', + 'onpopstate', + 'onprogress', + 'onratechange', + 'onrejectionhandled', + 'onreset', + 'onresize', + 'onscroll', + 'onsearch', + 'onseeked', + 'onseeking', + 'onselect', + 'onstalled', + 'onstorage', + 'onsubmit', + 'onsuspend', + 'ontimeupdate', + 'ontoggle', + 'ontransitionend', + 'onunhandledrejection', + 'onunload', + 'onvolumechange', + 'onwaiting', + 'onwebkitanimationend', + 'onwebkitanimationiteration', + 'onwebkitanimationstart', + 'onwebkittransitionend', + 'onwheel', + 'open', + 'openDatabase', + 'opener', + 'origin', + 'outerHeight', + 'outerWidth', + 'pageXOffset', + 'pageYOffset', + 'parent', + 'performance', + 'personalbar', + 'postMessage', + 'print', + 'releaseEvents', + 'resizeBy', + 'resizeTo', + 'screen', + 'screenLeft', + 'screenTop', + 'screenX', + 'screenY', + 'scroll', + 'scrollbars', + 'scrollBy', + 'scrollTo', + 'scrollX', + 'scrollY', + 'self', + 'speechSynthesis', + 'status', + 'statusbar', + 'stop', + 'styleMedia', + 'toolbar', + 'top', + 'visualViewport', + 'webkitRequestFileSystem', + 'webkitResolveLocalFileSystemURL', + 'webkitStorageInfo', + ].map(function f(name) { + return { + name: name, + message: + 'Avoid using implicitly global variables. Use e.g. window.' + + name + + ' instead if this was your intent.', + }; + }), + ].flat(), }, }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2dd8f4b55b..3f17c9591f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -36,6 +36,15 @@ yarn.lock @backstage/reviewers @backst /plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva /plugins/cost-insights @backstage/reviewers @backstage/silver-lining /plugins/cost-insights-* @backstage/reviewers @backstage/silver-lining +/plugins/events-backend @backstage/reviewers @pjungermann +/plugins/events-backend-module-aws-sqs @backstage/reviewers @pjungermann +/plugins/events-backend-module-azure @backstage/reviewers @pjungermann +/plugins/events-backend-module-bitbucket-cloud @backstage/reviewers @pjungermann +/plugins/events-backend-module-gerrit @backstage/reviewers @pjungermann +/plugins/events-backend-module-github @backstage/reviewers @pjungermann +/plugins/events-backend-module-gitlab @backstage/reviewers @pjungermann +/plugins/events-backend-test-utils @backstage/reviewers @pjungermann +/plugins/events-node @backstage/reviewers @pjungermann /plugins/explore @backstage/reviewers @backstage/sda-se-reviewers /plugins/explore-react @backstage/reviewers @backstage/sda-se-reviewers /plugins/fossa @backstage/reviewers @backstage/sda-se-reviewers diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml new file mode 100644 index 0000000000..782b5fdfa0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -0,0 +1,84 @@ +name: '🐛 Bug Report' +description: 'Submit a bug report to help us improve' +title: '🐛 Bug Report: ' +labels: + - bug +body: + - type: markdown + attributes: + value: We value your time and effort to submit this bug report. 🙏 + - type: textarea + id: description + validations: + required: true + attributes: + label: '📜 Description' + description: 'A clear and concise description of what the bug is.' + placeholder: 'It bugs out when ...' + - type: textarea + id: expected-behavior + validations: + required: true + attributes: + label: '👍 Expected behavior' + description: 'What did you think should happen?' + placeholder: 'It should ...' + - type: textarea + id: actual-behavior + validations: + required: true + attributes: + label: '👎 Actual Behavior with Screenshots' + description: 'What did actually happen? Add screenshots, if applicable.' + placeholder: 'It actually ...' + - type: textarea + id: steps-to-reproduce + validations: + required: true + attributes: + label: '👟 Reproduction steps' + description: 'How do you trigger this bug? Please walk us through it step by step.' + placeholder: + "Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code or configuration to reproduce, if relevant.\n + 1. Go to '...'\n + 2. Click on '....'\n + 3. Scroll down to '....'" + - type: textarea + id: context + validations: + required: false + attributes: + label: '📃 Provide the context for the Bug.' + description: 'How has this issue affected you? What are you trying to accomplish?' + placeholder: 'Providing context (e.g. links to configuration settings, stack trace or log data) helps us come up with a solution that is most useful in the real world.' + - type: textarea + id: environment + validations: + required: false + attributes: + label: '🖥️ Your Environment' + description: 'Provide Browser Information + Provide Output of `yarn backstage-cli info`' + placeholder: 'Include as many relevant details about the environment you experienced the bug in.' + - type: checkboxes + id: no-duplicate-issues + attributes: + label: '👀 Have you spent some time to check if this bug has been raised before?' + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: '🏢 Have you read the Code of Conduct?' + options: + - label: 'I have read the [Code of Conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md)' + required: true + - type: dropdown + attributes: + label: Are you willing to submit PR? + description: This is absolutely not required, but we are happy to guide you in the contribution process. + options: + - Yes I am willing to submit a PR! + - No, but I'm happy to collaborate on a PR with someone else + - No, I don't have time to work on this right now diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md deleted file mode 100644 index 04f8fd68d0..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_template.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -name: 'Bug Report' -about: 'Create Bug Report' -labels: bug ---- - - - -## Expected Behavior - - - -## Actual Behavior - - - -## Steps to Reproduce - - - - -1. Step 1 -2. Step 2 -3. ... - -## Context - - - - - -## Your Environment - - - -- Browser Information: - -- Output of `yarn backstage-cli info`: - -```text - -``` diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml new file mode 100644 index 0000000000..b5e06d48cf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -0,0 +1,54 @@ +name: 🚀 Feature +description: 'Submit a proposal for a new feature' +title: '🚀 Feature: ' +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + We value your time and efforts to submit this Feature request form. 🙏 + - type: textarea + id: feature-description + validations: + required: true + attributes: + label: '🔖 Feature description' + description: 'A clear and concise description of what the feature is.' + placeholder: 'You should add ...' + - type: textarea + id: context + validations: + required: true + attributes: + label: '🎤 Context' + description: 'Please explain why this feature should be implemented and how it would be used. Add examples, if applicable.' + placeholder: 'In my use-case, ...' + - type: textarea + id: implementation + attributes: + label: '✌️ Possible Implementation' + description: 'A clear and concise description of what you want to happen.' + placeholder: 'Not obligatory, but ideas as to the implementation of the addition or change' + - type: checkboxes + id: no-duplicate-issues + attributes: + label: '👀 Have you spent some time to check if this feature request has been raised before?' + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: '🏢 Have you read the Code of Conduct?' + options: + - label: 'I have read the [Code of Conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md)' + required: true + - type: dropdown + id: willing-to-submit-pr + attributes: + label: Are you willing to submit PR? + description: This is absolutely not required, but we are happy to guide you in the contribution process. + options: + - Yes I am willing to submit a PR! + - No, but I'm happy to collaborate on a PR with someone else + - No, I don't have time to work on this right now diff --git a/.github/ISSUE_TEMPLATE/feature_template.md b/.github/ISSUE_TEMPLATE/feature_template.md deleted file mode 100644 index d70622bf52..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_template.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: 'Feature Request' -about: 'Suggest new features and changes' -labels: enhancement ---- - - - -## Feature Suggestion - - - - -## Possible Implementation - - - -## Context - - - - diff --git a/.github/ISSUE_TEMPLATE/plugin.yaml b/.github/ISSUE_TEMPLATE/plugin.yaml new file mode 100644 index 0000000000..a36c427981 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/plugin.yaml @@ -0,0 +1,52 @@ +name: 🔌 Plugin +description: 'Submit a proposal for a new Plugin' +title: '🔌 Plugin: ' +labels: [plugin] +body: + - type: markdown + attributes: + value: | + We value your time and efforts to submit this Plugin request form. 🙏 + - type: textarea + id: plugin-summary + validations: + required: true + attributes: + label: '🔖 Summary' + description: 'Provide a general summary of the plugin and how it should work' + placeholder: 'You should add ...' + - type: textarea + id: website + attributes: + label: '🌐 Project website (if applicable)' + description: 'Add a link to the open source project or product this plugin will integrate with, if existing' + placeholder: 'Website Link is ...' + - type: textarea + id: context + attributes: + label: '✌️ Context' + description: 'A clear and concise description about the Plugin.' + placeholder: 'Providing additional context' + - type: checkboxes + id: no-duplicate-issues + attributes: + label: '👀 Have you spent some time to check if this plugin request has been raised before?' + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: '🏢 Have you read the Code of Conduct?' + options: + - label: 'I have read the [Code of Conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md)' + required: true + - type: dropdown + id: willing-to-submit-pr + attributes: + label: Are you willing to submit PR? + description: This is absolutely not required, but we are happy to guide you in the contribution process. + options: + - Yes I am willing to submit a PR! + - No, but I'm happy to collaborate on a PR with someone else + - No, I don't have time to work on this right now diff --git a/.github/ISSUE_TEMPLATE/plugin_template.md b/.github/ISSUE_TEMPLATE/plugin_template.md deleted file mode 100644 index 75f0828848..0000000000 --- a/.github/ISSUE_TEMPLATE/plugin_template.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: 'Plugin suggestion' -about: 'Suggest a new Backstage plugin' -title: '[Plugin] THE PLUGIN NAME' -labels: plugin ---- - - - - -## Summary - - - -## Project website (if applicable) - - - -## Context - - diff --git a/.github/ISSUE_TEMPLATE/rfc.yaml b/.github/ISSUE_TEMPLATE/rfc.yaml new file mode 100644 index 0000000000..775c64e674 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/rfc.yaml @@ -0,0 +1,51 @@ +name: 💬 RFC +description: 'Request For Comments (RFC) from the community' +title: '💬 RFC: ' +labels: [rfc] +body: + - type: markdown + attributes: + value: | + We value your time and efforts to submit this RFC form. 🙏 + - type: textarea + id: rfc-need + validations: + required: true + attributes: + label: '🔖 Need' + description: 'Let us know why are you proposing this change' + placeholder: 'The problem we’re trying to address and the benefits/impact we expect to get from this are ...' + - type: textarea + id: proposal + validations: + required: true + attributes: + label: '🎉 Proposal' + description: 'Describe the proposal in as much detail as needed for reviewers to give concrete feedback.' + placeholder: 'Take special care in this section to describe any implications on data privacy or security.' + - type: textarea + id: alternatives + attributes: + label: '〽️ Alternatives' + description: 'What alternatives to the proposed solution were considered?' + placeholder: 'What criteria/data was used to discard these?' + - type: textarea + id: risk + attributes: + label: '❌ Risks' + description: 'What other things happening could conflict or compete (for example for resources) with the proposal?' + placeholder: 'What risk are there and how do we plan to handle them?' + - type: checkboxes + id: no-duplicate-issues + attributes: + label: '👀 Have you spent some time to check if this RFC has been raised before?' + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: '🏢 Have you read the Code of Conduct?' + options: + - label: 'I have read the [Code of Conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md)' + required: true diff --git a/.github/ISSUE_TEMPLATE/rfc_template.md b/.github/ISSUE_TEMPLATE/rfc_template.md deleted file mode 100644 index c4990ee5d6..0000000000 --- a/.github/ISSUE_TEMPLATE/rfc_template.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: 'RFC' -about: 'Request For Comments (RFC) from the community' -labels: rfc -title: '[RFC] ' ---- - -**Status:** Open for comments - - - -## Need - - - -## Proposal - - - -## Alternatives - - - -## Risks - - diff --git a/.github/ISSUE_TEMPLATE/ux-component_template.md b/.github/ISSUE_TEMPLATE/ux-component_template.md deleted file mode 100644 index 95b8f47615..0000000000 --- a/.github/ISSUE_TEMPLATE/ux-component_template.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: 'UX Component' -about: 'For designers to request UX components to be added to the Backstage Storybook' -labels: design ---- - -## 🗒 General - - - - -## 💻 Usage - - - -## 📐 Specs - - - - -## 🔮 Future - - diff --git a/.github/ISSUE_TEMPLATE/ux_component.yaml b/.github/ISSUE_TEMPLATE/ux_component.yaml new file mode 100644 index 0000000000..453f1e8278 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ux_component.yaml @@ -0,0 +1,53 @@ +name: 🦄 UX Component +description: 'For designers to request UX components to be added to the Backstage Storybook' +title: '🦄 UX Component: ' +labels: [design] +body: + - type: markdown + attributes: + value: | + We value your time and efforts to submit this RFC form. 🙏 + - type: textarea + id: ux-general + validations: + required: true + attributes: + label: '🔖 General' + description: 'Write a nice note to the community requesting the creation of a new component.' + placeholder: 'Include an image of your component. Bonus points for a GIF!' + - type: textarea + id: usage + validations: + required: true + attributes: + label: '💻 Usage' + description: "Tell us what's the point of this component/pattern is." + placeholder: 'How does it help? How should it work? Any rules?' + - type: textarea + id: specs + validations: + required: true + attributes: + label: '📐 Specs' + description: 'Include images that detail the redlines for your component.' + placeholder: "Once we get our Figma workspace set up, we'll be posting the Figma files rather than doing specs by hand." + - type: textarea + id: future + attributes: + label: '🔮 Future' + description: 'List out any upcoming, exciting functionality for this component.' + placeholder: 'The component will have ...' + - type: checkboxes + id: no-duplicate-issues + attributes: + label: '👀 Have you spent some time to check if this UX Component request has been raised before?' + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + id: read-code-of-conduct + attributes: + label: '🏢 Have you read the Code of Conduct?' + options: + - label: 'I have read the [Code of Conduct](https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md)' + required: true diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 5c4745c70c..b58c0beb05 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,6 +1,7 @@ { labels: ['dependencies'], extends: ['config:base', ':disableDependencyDashboard', ':gitSignOff'], + postUpdateOptions: ['yarnDedupeHighest'], rangeStrategy: 'update-lockfile', // @elastic/elasticsearch is ignored due to licensing issues. See #10992 ignoreDeps: ['@elastic/elasticsearch', 'event-source-polyfill'], @@ -20,5 +21,10 @@ groupName: 'SVGR monorepo packages', rangeStrategy: 'replace', }, + // We update yarn packages manually as it's gzip'd and we don't want to pollute the repository too much. + { + matchSourceUrls: ['https://github.com/yarnpkg/berry'], + enabled: false, + }, ], } diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index d21d0f9f25..bba59094e3 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -4,9 +4,11 @@ ACLs addon addons ADRs +airbrake Airbrake Airbrakes -airbrake +Alaria +Alef Anddddd Apdex api @@ -26,9 +28,9 @@ Billett bitbucket Bitrise Blackbox -Brex bool boolean +Brex builtins callout CDNs @@ -42,6 +44,7 @@ CI/CD classname cli cloudbuild +Cloudflare Cloudformation cncf Cobertura @@ -49,11 +52,11 @@ codeblocks Codecov codehilite Codehilite -codescene -CodeScene codemod codemods codeowners +codescene +CodeScene composability composable config @@ -75,8 +78,8 @@ Debounce debuggability declaratively deduplicated -deps dependabot +deps destructured destructuring dev @@ -95,9 +98,9 @@ don'ts dynatrace Dynatrace ecco +elasticsearch env Env -elasticsearch esbuild eslint ESModule @@ -117,12 +120,13 @@ Francesco gerrit Gerrit gitbeaker +gitea +Gitea github Gitiles gitlab GitLab -gitea -Gitea +globals Gource Grafana graphql @@ -140,6 +144,7 @@ hotspots http https Iain +Iglesias iLert img incentivised @@ -153,12 +158,12 @@ JaCoCo JavaScript jenkins Jira -JWTs jq js json jsonnet jsx +JWTs Kaewkasi Keyv Knex @@ -230,11 +235,14 @@ onboarding Onboarding OpenShift orgs +padding +paddings pagerduty pageview parallelization parseable Patrik +pattison Peloton performant Performant @@ -268,14 +276,13 @@ rebase Recharts Redash replicasets -statefulsets repo Repo repos rerender rerenders -Reusability reusability +Reusability roadmaps rollbar Rollbar @@ -298,9 +305,9 @@ semver serializable Serverless shoutout -siloed SIG SIGs +siloed Sinon Snyk Sonarqube @@ -311,11 +318,14 @@ Spotifiers spotify Spotify sqlite +sqs squidfunk src +statefulsets stdout -stringify +storable stringified +stringify subcomponent subcomponents subfolder @@ -329,10 +339,10 @@ superfences Superfences superset supertype -storable SVGs talkdesk Talkdesk +Tanzu tasklist techdocs Telenor @@ -347,6 +357,7 @@ theia thumbsup todo todos +togglable tolerations Tolerations toolchain @@ -374,9 +385,11 @@ upvote URIs URLs utils +Valentina validator validators varchar +VMware VPCs VSCode Wayfair @@ -393,7 +406,8 @@ yaml Zalando Zhou zod +Zolotusky zoomable zsh -Alef -Cloudflare +Lainfiesta +allowlisted diff --git a/.github/workflows/automate_merge_message.yml b/.github/workflows/automate_merge_message.yml new file mode 100644 index 0000000000..9fed796aa5 --- /dev/null +++ b/.github/workflows/automate_merge_message.yml @@ -0,0 +1,63 @@ +name: Automate merge message +on: + pull_request_target: + branches: ['master'] + types: ['closed'] + +permissions: + pull-requests: write + actions: none + checks: none + contents: none + deployments: none + issues: none + packages: none + pages: none + repository-projects: none + security-events: none + statuses: none + +jobs: + message: + # prevent running towards forks, and only run on merged PRs + if: github.repository == 'backstage/backstage' && github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: '${{ github.event.pull_request.merge_commit_sha }}' + + - name: fetch head & base + run: git fetch --depth 1 origin ${{ github.event.pull_request.head.sha }} ${{ github.event.pull_request.base.sha }} + + # We avoid using the in-source script since this workflow has elevated permissions that we don't want to expose + - name: Generate Message + id: generate-message + run: | + rm -f generate.js + wget -O generate.js https://raw.githubusercontent.com/backstage/backstage/master/scripts/generate-merge-message.js 1>&2 + node generate.js ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} > message.txt + + - name: Post Message + uses: actions/github-script@v6 + env: + ISSUE_NUMBER: ${{ github.event.pull_request.number }} + with: + script: | + const owner = "backstage"; + const repo = "backstage"; + const body = require('fs').readFileSync('message.txt', 'utf8').trim(); + const issue_number = Number(process.env.ISSUE_NUMBER); + + if (!body) { + console.log(`skipping comment for #${issue_number}`); + return; + } + + console.log(`creating comment for #${issue_number}`); + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); diff --git a/.github/workflows/automate_stale.yml b/.github/workflows/automate_stale.yml index 51bf41d61a..1d8aa525a9 100644 --- a/.github/workflows/automate_stale.yml +++ b/.github/workflows/automate_stale.yml @@ -8,7 +8,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v5 + - uses: actions/stale@v6 id: stale with: stale-issue-message: > @@ -17,7 +17,7 @@ jobs: for your contributions. days-before-issue-stale: 60 days-before-issue-close: 7 - exempt-issue-labels: pinned,security,plugin,after-vacations,rfc,will-fix + exempt-issue-labels: plugin,after-vacations,will-fix stale-issue-label: stale stale-pr-message: > This PR has been automatically marked as stale because it has not had diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64067188bb..7cc5874184 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.x, 16.x] + node-version: [16.x, 18.x] env: CI: true @@ -33,7 +33,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -47,7 +47,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.x, 16.x] + node-version: [16.x, 18.x] env: CI: true @@ -63,10 +63,13 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} + - name: verify yarn dependency duplicates + run: node scripts/verify-lockfile-duplicates.js + - name: verify changesets run: node scripts/verify-changesets.js @@ -121,7 +124,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [14.x, 16.x] + node-version: [16.x, 18.x] services: postgres13: @@ -178,7 +181,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -208,7 +211,7 @@ jobs: - name: test all packages (and upload coverage) if: ${{ steps.yarn-lock.outcome == 'failure' }} run: | - yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=1300M --coverage + yarn backstage-cli repo test --maxWorkers=2 --workerIdleMemoryLimit=800M --coverage bash <(curl -s https://codecov.io/bash) -N $(git rev-parse FETCH_HEAD) env: BACKSTAGE_NEXT_TESTS: 1 diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 467c817baf..2d28c242ee 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -8,7 +8,7 @@ jobs: cron: runs-on: ubuntu-latest steps: - - uses: backstage/actions/cron@v0.5.6 + - uses: backstage/actions/cron@v0.5.8 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index 90ea8e13f1..f5dc164737 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -9,13 +9,13 @@ jobs: steps: - name: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: backstage ref: v${{ github.event.client_payload.version }} - name: setup-node - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: 16.x registry-url: https://registry.npmjs.org/ @@ -23,7 +23,7 @@ jobs: # Beginning of yarn setup, keep in sync between all workflows. # TODO(Rugvip): move this to composite action once all features we use are supported - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -31,7 +31,7 @@ jobs: # Cache every node_modules folder inside the monorepo - name: cache all node_modules id: cache-modules - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: '**/node_modules' # We use both yarn.lock and package.json as cache keys to ensure that @@ -44,10 +44,10 @@ jobs: - name: find location of global yarn cache id: yarn-cache if: steps.cache-modules.outputs.cache-hit != 'true' - run: echo "::set-output name=dir::$(yarn cache dir)" + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: cache global yarn cache - uses: actions/cache@v2 + uses: actions/cache@v3 if: steps.cache-modules.outputs.cache-hit != 'true' with: path: ${{ steps.yarn-cache.outputs.dir }} diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index af6f222377..1f87c30af2 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [16.x] env: CI: true diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index 476e5dd10b..053f3db621 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [16.x] env: CI: true @@ -26,7 +26,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index f3736d93a2..bebe24d0f6 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -12,8 +12,9 @@ jobs: needs_release: ${{ steps.release_check.outputs.needs_release }} strategy: + fail-fast: false matrix: - node-version: [14.x, 16.x] + node-version: [16.x, 18.x] services: postgres13: @@ -67,7 +68,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -130,7 +131,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [16.x] env: CI: 'true' @@ -145,7 +146,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/issue.yaml b/.github/workflows/issue.yaml index 95e91dda30..e7582bd788 100644 --- a/.github/workflows/issue.yaml +++ b/.github/workflows/issue.yaml @@ -10,4 +10,4 @@ jobs: if: github.repository == 'backstage/backstage' steps: - name: Issue sync - uses: backstage/actions/issue-sync@v0.5.6 + uses: backstage/actions/issue-sync@v0.5.8 diff --git a/.github/workflows/pr-review-comment.yaml b/.github/workflows/pr-review-comment.yaml index e822209e03..e3988a898d 100644 --- a/.github/workflows/pr-review-comment.yaml +++ b/.github/workflows/pr-review-comment.yaml @@ -33,9 +33,9 @@ jobs: } const prNumber = artifact.name.slice('pr_number-'.length) - console.log(`::set-output name=pr-number::${prNumber}`); + core.setOutput('pr-number', prNumber); - - uses: backstage/actions/re-review@v0.5.6 + - uses: backstage/actions/re-review@v0.5.8 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index e13928ccee..54fcf2e74b 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -18,7 +18,7 @@ jobs: if: github.repository == 'backstage/backstage' && ( github.event.pull_request || github.event.issue.pull_request ) steps: - name: PR sync - uses: backstage/actions/pr-sync@v0.5.6 + uses: backstage/actions/pr-sync@v0.5.8 with: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index 779284818f..45c84e298a 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -20,7 +20,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index dd35ba26f3..7b01757ba7 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [16.x] steps: - uses: actions/checkout@v3 @@ -23,7 +23,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml index 736f09571f..dc1d942e32 100644 --- a/.github/workflows/verify_docs-quality.yml +++ b/.github/workflows/verify_docs-quality.yml @@ -18,7 +18,7 @@ jobs: # also contains an "--config=.github/vale/config.ini" option - name: generate vale args id: generate - run: echo "::set-output name=args::$(node scripts/check-docs-quality.js --ci-args)" + run: echo "args=$(node scripts/check-docs-quality.js --ci-args)" >> $GITHUB_OUTPUT - name: documentation quality check uses: errata-ai/vale-action@v2.0.1 diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 3353a84f3b..8d296041b7 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -31,7 +31,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - node-version: [14.x, 16.x] + node-version: [16.x, 18.x] env: CI: true @@ -52,7 +52,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index e1bb2246dc..59c8d9f7cc 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x] + node-version: [16.x, 18.x] env: CI: true @@ -26,7 +26,9 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.9' - name: install dependencies run: yarn install --immutable diff --git a/.github/workflows/verify_e2e-tugboat.yml b/.github/workflows/verify_e2e-tugboat.yml index 04d9e686e2..cb3a463a0d 100644 --- a/.github/workflows/verify_e2e-tugboat.yml +++ b/.github/workflows/verify_e2e-tugboat.yml @@ -52,7 +52,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: '14' + node-version: '16.x' - name: yarn install run: yarn --cwd cypress install diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 4e8025f7d5..8a49b8fe31 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [windows-2019] - node-version: [14.x, 16.x] + node-version: [16.x, 18.x] env: CI: true @@ -49,7 +49,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.0.3 + uses: microsoft/setup-msbuild@v1.1.3 - name: setup chrome uses: browser-actions/setup-chrome@latest - name: yarn install diff --git a/.github/workflows/verify_kubernetes.yml b/.github/workflows/verify_kubernetes.yml index e64ddec586..a50f817020 100644 --- a/.github/workflows/verify_kubernetes.yml +++ b/.github/workflows/verify_kubernetes.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x] + node-version: [16.x, 18.x] env: CI: true @@ -28,12 +28,12 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: bootstrap kind - uses: helm/kind-action@v1.3.0 + uses: helm/kind-action@v1.4.0 - name: kubernetes test working-directory: packages/backend-common diff --git a/.github/workflows/verify_microsite.yml b/.github/workflows/verify_microsite.yml index 50c2116090..4085f334f3 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [16.x] env: CI: true @@ -32,6 +32,9 @@ jobs: run: yarn install --immutable working-directory: microsite + - name: verify yarn dependency duplicates + run: node scripts/verify-lockfile-duplicates.js + - name: prettier run: yarn prettier:check working-directory: microsite diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index cac94ff682..b3bd850937 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -35,7 +35,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.6 + uses: backstage/actions/yarn-install@v0.5.8 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: storybook yarn install diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index 8bbaa645bf..75a0633cbe 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x] + node-version: [16.x, 18.x] env: CI: true diff --git a/ADOPTERS.md b/ADOPTERS.md index cc5db5dea7..90aaa5c69b 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -141,7 +141,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Kambi AB](https://www.kambi.com) | [Martin Norum](mailto:martin.norum@kambi.com) | We want to kick ass at speed, so we're currently building up a catalog of our existing software, and looking into how Backstage can support us in our journey towards autonomous product teams. Both to improve speed to market and operational awareness. | | [ANZ](https://www.anz.com.au/personal/) | [Elliot Jackson](mailto:elliot.jackson@anz.com) | Catalog, tech docs and automation | | [Genie Solutions](https://www.geniesolutionssoftware.com.au) | [Zainab Bagasrawala](mailto:zainabbagasrawala@geniesolutions.com.au) | Developer Portal to track our projects, documentation, observability tools and more | -| [MadeiraMadeira](https://www.madeiramadeira.com.br) | [Paulo Eduardo Peixoto](mailto:paulo.peixoto@madeiramadeira.com.br) | As a support tool for developers, following the principles of "Developer Experience". In order to make the developer's day to day more practical, efficient and, why not, happy. | +| [MadeiraMadeira](https://www.madeiramadeira.com.br) | [DX Team](mailto:dxteam@madeiramadeira.com.br) | As a support tool for developers, following the principles of "Developer Experience". In order to make the developer's day to day more practical, efficient and, why not, happy. | | [Sonatype](https://www.sonatype.com) | [Srikar Ananthula](mailto:sananthula@sonatype.com) | Centralize services used internally with many plugins | | [CVS Health](https://www.cvshealth.com) | [Ari Ben-Elazar](mailto:abenelazar@gmail.com) | Cataloging and documenting our service offerings to offer our internal developers a better operational journey | | [Yatra.com](https://www.yatra.com) | [Matiur Rahman Maitur](mailto:arifrahman4u@gmail.com) | Easy to find out Project details, ownership, dependent services, Documentation, it is very useful for developer. | @@ -215,4 +215,10 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Ferrovial](https://ferrovial.com) | [Jose Luis Rosado](mailto:jlrosado@ferrovial.com) | Backstage is helping us to improve and acelerate dev experience helping teams to quickly find technical documentation, infrastructure templates, pipelines, software components and quickstarters that have been developed by our squads in a inner source friendly environment. | | [Inter&Co](https://bancointer.com.br) | [Arnaud Lanna](https://github.com/arnaudlanna), [Adriano Silva](https://github.com/adrianovss), [Bruno Grossi](https://github.com/begrossi) | We're using Backstage as our internal Developer Portal to catalog and collect repositories and microservices pieces of information like ownership, deployment time, and documentation. | | [StatusNeo](https://statusneo.com/) | [Karan Nangru](mailto:nangru@statusneo.com), [@NishkarshRaj](https://github.com/NishkarshRaj), and [Gaurav Sarien](mailto:gaurav.sarien@statusneo.com) | Harnessing the power of central catalog inventory and self-serving software templates | -| [Alaska Airlines](https://alaskaair.com) | [@swerdick](https://github.com/swerdick) | Backstage is the developer portal for our 'software delivery platform'. Consolidating developer tools to one place, and providing automation to make it easy for developers to create and deploy applications to Kubernetes | +| [Alaska Airlines](https://alaskaair.com) | [@swerdick](https://github.com/swerdick) | Backstage is the developer portal for our 'software delivery platform'. Consolidating developer tools to one place, and providing automation to make it easy for developers to create and deploy applications to Kubernetes +| [Loft](https://loft.com.br) | [Squad DevTools](mailto:squad_devtools@loft.com.br) | We're using Backstage to give visibility and promote ownership of all our applications, resources and tools. Now moving to use it as a Developer Portal to create applications, AWS resources etc. | +| [Raízen](https://www.raizen.com) | [Melquisedque Bernardes Pereira](https://github.com/rayleshh) and [Paulo Eduardo Peixoto](https://github.com/padupe) | Backstage helps us to organize and make available, in a simple and direct way, all the infrastructure for new projects. In addition, it has become a great support tool for our developers. | +| [Trifork](https://trifork.com) | [Casper Thygesen](https://github.com/cthtrifork) | We're using Backstage as part of our dataplatform product. It integrates with the infrastructure components and is the developer portal for all the platform users. | +| [MSCI](http://msci.com) | [Stephen Burrows](mailto:stephen.burrows@msci.com) | Developer portal, service catalog, documentation and tooling +| [ESW](https://esw.com) | [Alisson Fabiano](https://github.com/afabianoo), [Bruno Quintella](https://github.com/quintelab) and [Guilherme Oenning](https://github.com/goenning) | Backstage is our one stop shop to find everything related to all our services, such as ownership, dependencies, production status, tech health and much more. +| [FanDuel](https://fanduel.com) | [Diego Herrera](https://github.com/diegoh), [Christy Campbell](https://github.com/FD-ChristopherCampbell) | We use backstage as our developer portal to provide visibility of our software, ownership, strategy, and the state of maturity across disciplines. diff --git a/README.md b/README.md index 9a3c4cdb5a..8a2bd338d9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Discord](https://img.shields.io/discord/687207715902193673)](https://discord.gg/EBHEGzX) ![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) [![Codecov](https://img.shields.io/codecov/c/github/backstage/backstage)](https://codecov.io/gh/backstage/backstage) -[![](https://img.shields.io/npm/v/@backstage/core?label=Version)](https://github.com/backstage/backstage/releases) +[![](https://img.shields.io/github/v/release/backstage/backstage)](https://github.com/backstage/backstage/releases) ## What is Backstage? @@ -52,7 +52,7 @@ Check out [the documentation](https://backstage.io/docs/getting-started) on how - [Code of Conduct](CODE_OF_CONDUCT.md) - This is how we roll - [Adopters](ADOPTERS.md) - Companies already using Backstage - [Blog](https://backstage.io/blog/) - Announcements and updates -- [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to our email newsletter +- [Newsletter](https://spoti.fi/backstagenewsletter) - Subscribe to our email newsletter - [Backstage Community Sessions](https://github.com/backstage/community) - Join monthly meetups and explore Backstage community - Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️ diff --git a/REVIEWING.md b/REVIEWING.md index 469ac249f7..500e15701d 100644 --- a/REVIEWING.md +++ b/REVIEWING.md @@ -171,6 +171,37 @@ Exported types can be marked with either `@public`, `@alpha` or `@beta` release If a package does not have this configuration, then all exported types are considered stable, even if they are marked as `@alpha` or `@beta`. +#### Changes that are Not Considered Breaking + +There are a few exceptions that are not considered breaking in the [versioning policy](https://backstage.io/docs/overview/versioning-policy#changes-that-are-not-considered-breaking), +primarily regarding Utility APIs and Backend Service interfaces (referred to "contracts" below) that are supplied by the +Backstage core packages. + +Example of a non-breaking change to a contract which has a default +implementation, since consumers typically only interact with that contract as +callers of existing methods: + +```diff + export interface MyService { + oldMethod(): void; ++ newMethod(): void; + } +``` + +Changes such as these must still be marked with `**BREAKING PRODUCERS**:` in the +changelog, to highlight them for those who might be implementing custom +implementations of those contracts or putting mocks of the contract in tests. + +Example of a breaking change to a contract, which affects existing consumers and +therefore makes it NOT fall under these exceptions: + +```diff + export interface MyService { +- oldMethod(): void; ++ oldMethod(): Promise; + } +``` + #### Type Contract Direction An important distinction to make when looking at changes to an API Report is the direction of the contract of a changed type, that is, whether it's used as input or output from the user's point of view. In the next two sections we'll dive into the different directions of a type contract, and how it affects whether a change is breaking or not. diff --git a/app-config.yaml b/app-config.yaml index 69a1207365..dd72051189 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -1,7 +1,6 @@ app: title: Backstage Example App baseUrl: http://localhost:3000 - googleAnalyticsTrackingId: # UA-000000-0 #datadogRum: # clientToken: '123456789' # applicationId: qwerty @@ -374,6 +373,7 @@ auth: development: {} costInsights: engineerCost: 200000 + engineerThreshold: 0.5 products: computeEngine: name: Compute Engine diff --git a/contrib/chart/backstage/templates/backstage-app-config.yaml b/contrib/chart/backstage/templates/backstage-app-config.yaml index f06e47feab..061fc3285b 100644 --- a/contrib/chart/backstage/templates/backstage-app-config.yaml +++ b/contrib/chart/backstage/templates/backstage-app-config.yaml @@ -13,7 +13,6 @@ metadata: data: APP_CONFIG_app_baseUrl: {{ .Values.appConfig.app.baseUrl | quote | quote }} APP_CONFIG_app_title: {{ .Values.appConfig.app.title | quote | quote }} - APP_CONFIG_app_googleAnalyticsTrackingId: {{ .Values.appConfig.app.googleAnalyticsTrackingId | quote | quote }} APP_CONFIG_backend_baseUrl: {{ .Values.appConfig.backend.baseUrl | quote | quote }} APP_CONFIG_backend_cors_origin: {{ .Values.appConfig.backend.cors.origin | quote | quote }} APP_CONFIG_techdocs_storageUrl: {{ .Values.appConfig.techdocs.storageUrl | quote | quote }} diff --git a/contrib/chart/backstage/values.yaml b/contrib/chart/backstage/values.yaml index 6ffe076e57..cd80fd954b 100644 --- a/contrib/chart/backstage/values.yaml +++ b/contrib/chart/backstage/values.yaml @@ -92,7 +92,6 @@ appConfig: app: baseUrl: https://demo.example.com title: Backstage - googleAnalyticsTrackingId: backend: baseUrl: https://demo.example.com listen: diff --git a/contrib/docker/devops/Dockerfile b/contrib/docker/devops/Dockerfile index a7c194618a..8703b2ab48 100644 --- a/contrib/docker/devops/Dockerfile +++ b/contrib/docker/devops/Dockerfile @@ -1,4 +1,4 @@ -ARG IMAGE_TAG=14-alpine +ARG IMAGE_TAG=16-alpine FROM node:${IMAGE_TAG} diff --git a/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild b/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild index 41f931544f..053b4fb492 100644 --- a/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild +++ b/contrib/docker/frontend-with-nginx/Dockerfile.dockerbuild @@ -35,7 +35,7 @@ -FROM node:14-buster AS build +FROM node:16-buster AS build RUN mkdir /app COPY . /app diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index 6c5fabd709..49b6ae0a4d 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -4,19 +4,15 @@ The Backstage backend APIs are by default available without authentication. To a API requests from frontend plugins include an authorization header with a Backstage identity token acquired when the user logs in. By adding a middleware that verifies said token to be valid and signed by Backstage, non-authenticated requests can be blocked with a 401 Unauthorized response. -**NOTE**: Enabling this means that Backstage will stop working for guests, as no token is issued for them. +**NOTE**: Enabling this means that Backstage will stop working for guests, as no token is issued for them. If you have not done so already, you will also need to implement [service-to-service auth](https://backstage.io/docs/auth/service-to-service-auth). As techdocs HTML pages load assets without an Authorization header the code below also sets a token cookie when the user logs in (and when the token is about to expire). Create `packages/backend/src/authMiddleware.ts`: ```typescript -import { SingleHostDiscovery } from '@backstage/backend-common'; import type { Config } from '@backstage/config'; -import { - getBearerTokenFromAuthorizationHeader, - IdentityClient, -} from '@backstage/plugin-auth-node'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { NextFunction, Request, Response, RequestHandler } from 'express'; import { decodeJwt } from 'jose'; import { URL } from 'url'; @@ -45,11 +41,6 @@ export const createAuthMiddleware = async ( config: Config, appEnv: PluginEnvironment, ) => { - const discovery = SingleHostDiscovery.fromConfig(config); - const identity = IdentityClient.create({ - discovery, - issuer: await discovery.getExternalBaseUrl('auth'), - }); const baseUrl = config.getString('backend.baseUrl'); const secure = baseUrl.startsWith('https://'); const cookieDomain = new URL(baseUrl).hostname; @@ -61,13 +52,13 @@ export const createAuthMiddleware = async ( try { const token = getBearerTokenFromAuthorizationHeader(req.headers.authorization) || - (req.cookies.token as string | undefined); + (req.cookies?.token as string | undefined); if (!token) { res.status(401).send('Unauthorized'); return; } try { - req.user = await identity.authenticate(token); + req.user = await appEnv.identity.getIdentity({ request: req }); } catch { await appEnv.tokenManager.authenticate(token); } @@ -180,7 +171,12 @@ export async function setTokenCookie(url: string, identityApi: IdentityApi) { ``` ```typescript -// packages/app/src/App.tsx from a create-app deployment +// required types and packages for example below + +import type { IdentityApi } from '@backstage/core-plugin-api'; +import { discoveryApiRef, useApi } from '@backstage/core-plugin-api'; + +// additional packages/app/src/App.tsx from a create-app deployment import { setTokenCookie } from './cookieAuth'; diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md index 3cedf5634b..1bdf9cc367 100644 --- a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md +++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md @@ -48,7 +48,7 @@ Once you've saved the action, you should see an authentication flow be triggered ### Frontend The Backstage App needs a SignInPage when authentication is required. -When using ALB authentication Backstage will only be loaded once the user has successfully authenticated; we won't need to display a SignIn page, however we will need to create a dummy SignIn component that can refresh the token. +When using ALB authentication Backstage will only be loaded once the user has successfully authenticated; we won't need to display a SignIn page, however we will need to create a placeholder SignIn component that can refresh the token. - edit `packages/app/src/App.tsx` - import the following two additional definitions from `@backstage/core-plugin-api`: `useApi`, `configApiRef`; these will be used to check whether Backstage is running locally or behind an ALB @@ -59,7 +59,7 @@ import React from 'react'; import { UserIdentity } from '@backstage/core-components'; import { SignInPageProps } from '@backstage/core-app-api'; -const DummySignInComponent: any = (props: SignInPageProps) => { +const SampleSignInComponent: any = (props: SignInPageProps) => { const [error, setError] = React.useState(); const config = useApi(configApiRef); React.useEffect(() => { @@ -102,13 +102,13 @@ const DummySignInComponent: any = (props: SignInPageProps) => { }; ``` -- add `DummySignInComponent` as `SignInPage`: +- add `SampleSignInComponent` as `SignInPage`: ```ts const app = createApp({ ... components: { - SignInPage: DummySignInComponent, + SignInPage: SampleSignInComponent, ... }, ... diff --git a/contrib/docs/tutorials/aws-deployment.md b/contrib/docs/tutorials/aws-deployment.md index 3a76da2ed3..a767d292bf 100644 --- a/contrib/docs/tutorials/aws-deployment.md +++ b/contrib/docs/tutorials/aws-deployment.md @@ -40,7 +40,7 @@ image](https://backstage.io/docs/getting-started/deployment-docker) documentation to build a new Backstage Docker image: ```shell -$ yarn build +$ yarn build:backend $ yarn build-image --tag backstage ``` diff --git a/contrib/scaffolder/README.md b/contrib/scaffolder/README.md new file mode 100644 index 0000000000..e22c4b391f --- /dev/null +++ b/contrib/scaffolder/README.md @@ -0,0 +1,15 @@ +# Scaffolder contrib tools + +## Testing Templates with Dry-run + +Scaffolder templates support anything that backstage.io and custom actions can do, so testing them is hard without actually running the instance of Backstage that they're designed for. + +Please leave [feedback on the RFC](https://github.com/backstage/backstage/issues/14280) on your experience with this approach. + +The [command line script](template-testing-dry-run.md) might offer a way for you to do so using the dry-run API used for the Template Editor. Run it against a running instance, either locally or remote, and use it like + +```sh +scaffolder-dry http://localhost:7007/ template-directory values.yml output-directory +``` + +If you're using backend permissions, pass a front-end auth token from a current browser session via `--token $FRONTEND_TOKEN`. diff --git a/contrib/scaffolder/template-testing-dry-run.md b/contrib/scaffolder/template-testing-dry-run.md new file mode 100644 index 0000000000..70e0c572ab --- /dev/null +++ b/contrib/scaffolder/template-testing-dry-run.md @@ -0,0 +1,150 @@ +```js +/* + * 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. + */ + +/** + * A CLI that helps you test Backstage software templates + * + * @packageDocumentation + */ + +import { dirname, join, relative } from 'node:path'; +import { readFile, writeFile } from 'node:fs/promises'; +import { gzipSync } from 'node:zlib'; +import { ensureDir } from 'fs-extra'; +import { program } from 'commander'; +import fetch from 'node-fetch'; +import ora from 'ora'; +import readdir from 'recursive-readdir'; +import { parse } from 'yaml'; +import { version } from '../../../package.json'; +import type { ScaffolderDryRunResponse } from '@backstage/plugin-scaffolder'; + +const loadDirectoryContents = async (template_path: string) => { + const spinner = ora('Loading template').start(); + const files = await await readdir(template_path, ['.git']); + const contents = await Promise.all( + files.map(async p => { + return { + path: relative(template_path, p), + base64Content: (await readFile(p)).toString('base64'), + }; + }), + ); + spinner.succeed(); + return contents; +}; + +const writeResultContents = async ( + root: string, + directoryContents: ScaffolderDryRunResponse['directoryContents'], +) => { + const directories = new Set( + directoryContents.map(({ path }) => dirname(join(root, path))), + ); + await Promise.all(Array.from(directories).map(d => ensureDir(d))); + await Promise.all( + directoryContents.map(async ({ path, base64Content, executable }) => + writeFile(join(root, path), Buffer.from(base64Content, 'base64'), { + mode: executable ? 0o755 : 0o644, + }), + ), + ); +}; + +const api = async ( + bodyObj: Record, + { baseURL, token }: { baseURL: string; token: string | false }, +) => { + const body = gzipSync(JSON.stringify(bodyObj)); + return fetch(new URL('/api/scaffolder/v2/dry-run', baseURL), { + method: 'POST', + headers: { + Authorization: `Bearer ${token || ''}`, + 'Content-Type': 'application/json', + 'Content-Encoding': 'gzip', + }, + body, + }); +}; + +const handle = async ( + baseURL: string, + template_path: string, + data: string, + target: string, + { token }: { token: string | false }, +) => { + const directoryContents = await loadDirectoryContents(template_path); + const values = parse(await readFile(data, 'utf-8')); + const secrets = {}; + const template = parse( + await readFile(`${template_path}/template.yaml`, 'utf-8'), + ); + const response = await api( + { + directoryContents, + values, + secrets, + template, + }, + { baseURL, token }, + ); + if (!response.ok) { + const contentType = response.headers.get('content-type'); + if (contentType?.startsWith('application/json')) { + const responseData = await response.json(); + if (responseData.error) { + throw responseData.error; + } + if (responseData.errors) { + throw responseData.errors; + } + throw responseData; + } + throw await response.text(); + } + const { log, directoryContents: resultContents } = + (await response.json()) as ScaffolderDryRunResponse; + for (const logEntry of log) { + console.log(logEntry.body.message); + } + await writeResultContents(target, resultContents); +}; + +const main = async (argv: string[]) => { + program + .name('backstage-scaffolder') + .version(version) + .description('Creates a dry-run output of a given template') + .option('-t, --token ', 'JWT to use for auth') + .argument('url', 'URL of your Backstage instance') + .argument('template-path', 'Source directory of template') + .argument('data-path', 'YAML file with input to render') + .argument('target', 'Output directory') + .action(handle); + + await program.parseAsync(argv); + process.exit(); +}; + +process.on('unhandledRejection', rejection => { + console.error(rejection); + process.exit(1); +}); + +main(process.argv); +``` diff --git a/contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh b/contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh index d68d9e31c4..5c87d16d29 100755 --- a/contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh +++ b/contrib/scripts/upgrade-backstage-app/upgrade-backstage-app.sh @@ -9,6 +9,7 @@ if [ "$CURRENT_VERSION" == "$TARGET_VERSION" ]; then else echo "Attempting upgrade from Backstage $CURRENT_VERSION (create-app $CREATE_APP_CURRENT_VERSION) to $TARGET_VERSION ($CREATE_APP_TARGET_VERSION)" rm -rf .upgrade && mkdir .upgrade + git fetch https://github.com/backstage/upgrade-helper-diff.git '+refs/heads/*:refs/remotes/upgrade-helper/*' curl -s https://raw.githubusercontent.com/backstage/upgrade-helper-diff/master/diffs/$CREATE_APP_CURRENT_VERSION..$CREATE_APP_TARGET_VERSION.diff > .upgrade/upgrade.diff git apply -3 .upgrade/upgrade.diff git mergetool diff --git a/cypress/README.md b/cypress/README.md index 15338fe371..4212af398c 100644 --- a/cypress/README.md +++ b/cypress/README.md @@ -24,7 +24,7 @@ necessary environment variables: ```sh yarn tsc -yarn build +yarn build:backend yarn workspace example-backend build-image docker run -p 7007:7007 example-backend ``` diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index d9b4869ecd..9c4a63b76c 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -10,7 +10,7 @@ "incremental": true, "isolatedModules": true, "jsx": "react", - "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2020", "ESNext.Promise"], + "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2021", "ESNext.Promise"], "module": "ESNext", "moduleResolution": "node", "noEmit": true, @@ -31,7 +31,7 @@ "strictNullChecks": true, "strictPropertyInitialization": true, "stripInternal": true, - "target": "ES2019", + "target": "ES2021", "types": ["node", "cypress"] } } diff --git a/cypress/yarn.lock b/cypress/yarn.lock index 3b1358156e..49c85adb2b 100644 --- a/cypress/yarn.lock +++ b/cypress/yarn.lock @@ -414,8 +414,8 @@ __metadata: linkType: hard "cypress@npm:^10.0.0": - version: 10.10.0 - resolution: "cypress@npm:10.10.0" + version: 10.11.0 + resolution: "cypress@npm:10.11.0" dependencies: "@cypress/request": ^2.88.10 "@cypress/xvfb": ^1.2.4 @@ -461,7 +461,7 @@ __metadata: yauzl: ^2.10.0 bin: cypress: bin/cypress - checksum: 668a32534a527dba79754abbf98af176b80c539a12ec00058932ba2a19c794c7888323e59e738c30f726ad740c5451c31d02548a0cb7c1b1c8ad01c55a984ca2 + checksum: 938cc6a20f7eeace5c8e850d234904ee1651cbb36d94666fe600cf17ce964e73d4f7d8d944aab677491702a57364e6aceeb4fe8bcbd96147ff5e2b575a956fb2 languageName: node linkType: hard @@ -1415,22 +1415,22 @@ __metadata: linkType: hard "typescript@npm:^4.1.3": - version: 4.8.4 - resolution: "typescript@npm:4.8.4" + version: 4.9.3 + resolution: "typescript@npm:4.9.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 + checksum: 17b8f816050b412403e38d48eef0e893deb6be522d6dc7caf105e54a72e34daf6835c447735fd2b28b66784e72bfbf87f627abb4818a8e43d1fa8106396128dc languageName: node linkType: hard "typescript@patch:typescript@^4.1.3#~builtin": - version: 4.8.4 - resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=a1c5e5" + version: 4.9.3 + resolution: "typescript@patch:typescript@npm%3A4.9.3#~builtin::version=4.9.3&hash=a1c5e5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 563a0ef47abae6df27a9a3ab38f75fc681f633ccf1a3502b1108e252e187787893de689220f4544aaf95a371a4eb3141e4a337deb9895de5ac3c1ca76430e5f0 + checksum: ef65c22622d864497d0a0c5db693523329b3284c15fe632e93ad9aa059e8dc38ef3bd767d6f26b1e5ecf9446f49bd0f6c4e5714a2eeaf352805dc002479843d1 languageName: node linkType: hard diff --git a/docs/FAQ.md b/docs/FAQ.md index f695a59410..fcd5542f74 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -54,7 +54,7 @@ begun work on various aspects of all three phases. Looking at the [milestones for active issues](https://github.com/backstage/backstage/milestones) will also give you a sense of our progress. -### My company doesn't have thousands of developers or services. Is Backstage overkill? +### My company doesn't have thousands of developers or services. Is using Backstage excessive for our needs? Not at all! A core reason to adopt Backstage is to standardize how software is built at your company. It's easier to decide on those standards as a small diff --git a/docs/architecture-decisions/adr010-luxon-date-library.md b/docs/architecture-decisions/adr010-luxon-date-library.md index 65cabd256c..a7b287ae0d 100644 --- a/docs/architecture-decisions/adr010-luxon-date-library.md +++ b/docs/architecture-decisions/adr010-luxon-date-library.md @@ -10,7 +10,7 @@ description: Architecture Decision Record (ADR) for Luxon Date Library Date formatting (e.g. `a day ago`) and calculations are common within Backstage. Some of these useful features are not supported by the standard JavaScript `Date` object. The popular [Moment.js](https://momentjs.com/) library has been -commonly used to fill this gap but suffers from large bundle sizes and mutable +commonly used to fill this gap but is affected by large bundle sizes and mutable state issues. On top of this, `momentjs` is [being sunset](https://momentjs.com/docs/#/-project-status/) and the project recommends using one of the more modern alternative libraries. diff --git a/docs/assets/getting-started/sidebar-submenu-example.png b/docs/assets/getting-started/sidebar-submenu-example.png new file mode 100644 index 0000000000..0b30f0ca6a Binary files /dev/null and b/docs/assets/getting-started/sidebar-submenu-example.png differ diff --git a/docs/assets/software-templates/custom-field-explorer.png b/docs/assets/software-templates/custom-field-explorer.png new file mode 100644 index 0000000000..1af897ac8c Binary files /dev/null and b/docs/assets/software-templates/custom-field-explorer.png differ diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index 01c18f04cd..a43e2307e6 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -49,7 +49,7 @@ the user to initiate a login. This login request is done to the `/start` endpoint which is handled by the `start` method. The `start` method re-directs to the external auth provider who authenticates -the request and re-directs the request to the `/frame/handler` endpoint, which +the request and re-directs the request to the `/handler/frame` endpoint, which is handled by the `frameHandler` method. The `frameHandler` returns an HTML response, containing a script that does a diff --git a/docs/auth/cloudflare/access.md b/docs/auth/cloudflare/access.md index c7db308438..d47495180a 100644 --- a/docs/auth/cloudflare/access.md +++ b/docs/auth/cloudflare/access.md @@ -63,7 +63,7 @@ export default async function createPlugin( // and the access token and produces the Backstage token with the // relevant user info. async resolver({ profile, result }, ctx) { - // Somehow compute the Backstage token claims. Just some dummy code + // Somehow compute the Backstage token claims. Just some sample code // shown here, but you may want to query your LDAP server, or // https://.cloudflareaccess.com/cdn-cgi/access/get-identity // https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json/#groups-within-a-jwt diff --git a/docs/auth/github/provider.md b/docs/auth/github/provider.md index a8c6020cae..353b8ebe08 100644 --- a/docs/auth/github/provider.md +++ b/docs/auth/github/provider.md @@ -24,7 +24,7 @@ Settings for local development: - Application name: Backstage (or your custom app name) - Homepage URL: `http://localhost:3000` -- Authorization callback URL: `http://localhost:7007/api/auth/github` +- Authorization callback URL: `http://localhost:7007/api/auth/github/handler/frame` ## Configuration diff --git a/docs/auth/google/gcp-iap-auth.md b/docs/auth/google/gcp-iap-auth.md index 1ebfcaf15b..d6303ced6f 100644 --- a/docs/auth/google/gcp-iap-auth.md +++ b/docs/auth/google/gcp-iap-auth.md @@ -26,6 +26,7 @@ auth: providers: gcp-iap: audience: '/projects//global/backendServices/' + jwtHeader: x-custom-header # Optional: Only if you are using a custom header for the IAP JWT ``` You can find the project number and service ID in the Google Cloud Console. @@ -69,7 +70,7 @@ export default async function createPlugin( // and the IAP token and produces the Backstage token with the // relevant user info. async resolver({ profile, result: { iapToken } }, ctx) { - // Somehow compute the Backstage token claims. Just some dummy code + // Somehow compute the Backstage token claims. Just some sample code // shown here, but you may want to query your LDAP server, or // GSuite or similar, based on the IAP token sub/email claims const id = iapToken.email.split('@')[0]; diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index d96c9fc45a..a65f3d7899 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -49,7 +49,7 @@ A user identity within Backstage is built up from two pieces of information, a user [entity reference](../features/software-catalog/references.md), and a set of ownership entity references. When a user signs in, a Backstage token is generated with these two pieces of information, -which is then used to identity the user within the Backstage ecosystem. +which is then used to identify the user within the Backstage ecosystem. The user entity reference should uniquely identify the logged in user in Backstage. It is encouraged that a matching user entity also exists within the Software Catalog, diff --git a/docs/auth/index.md b/docs/auth/index.md index ab04d3ed87..5702e96cee 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -258,9 +258,58 @@ Passport-supported authentication method. ## Custom ScmAuthApi Implementation -If you are using any custom authentication providers, like for example one for GitHub Enterprise, then you are likely to need a custom implementation of the [`ScmAuthApi`](https://backstage.io/docs/reference/integration-react.scmauthapi). It is an API used to authenticate towards different SCM systems in a generic way, based on what resource is being accessed, and is used for example by the Scaffolder (Software Templates) and Catalog Import plugins. +The default `ScmAuthAPi` provides integrations for `github`, `gitlab`, `azure` and `bitbucket` and is created by the following code in `packages/app/src/apis.ts`: -To set up a custom `ScmAuthApi` implementation, you'll need to add an API factory entry to `packages/app/src/apis.ts`. The following example shows an implementation that supports both public GitHub via `githubAuthApi` as well as a GitHub Enterprise installation hosted at `ghe.example.com` via `gheAuthApi`: +```ts +ScmAuth.createDefaultApiFactory(); +``` + +If you require only a subset of these integrations, then you will need a custom implementation of the [`ScmAuthApi`](https://backstage.io/docs/reference/integration-react.scmauthapi). It is an API used to authenticate different SCM systems generically, based on what resource is being accessed, and is used for example, by the Scaffolder (Software Templates) and Catalog Import plugins. + +The first step is to remove the code that creates the default providers. + +```diff + import { + ScmIntegrationsApi, + scmIntegrationsApiRef, ++ ScmAuth, + } from '@backstage/integration-react'; + + export const apis: AnyApiFactory[] = [ +... ++ ScmAuth.createDefaultApiFactory(), +... + ]; +``` + +Then replace it with something like this, which will create an `ApiFactory` with only a github provider. + +```ts +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: scmAuthApiRef, + deps: { + githubAuthApi: githubAuthApiRef, + }, + factory: ({ githubAuthApi }) => + ScmAuth.merge( + ScmAuth.forGithub(githubAuthApi), + ), + }); +``` + +If you use any custom authentication integrations, a new provider can be added to the `ApiFactory`. + +The first step is to create a new authentication ref, which follows the naming convention of `xxxAuthApiRef`. The example below is for a new GitHub enterprise integration which can be defined either inside the app itself if it's only used for this purpose or inside a common internal package for APIs, such as `@internal/apis`: + +```ts +const gheAuthApiRef: ApiRef = + createApiRef({ + id: 'internal.auth.ghe', + }); +``` + +The new ref is then used to add a new provider to the ApiFactory: ```ts createApiFactory({ @@ -278,3 +327,14 @@ createApiFactory({ ), }); ``` + +Finally, you also need to add and configure another provider to the `auth-backend` using the provider ID, which in this example is `ghe`: + +```ts +import { providers } from '@backstage/plugin-auth-backend'; + +// Add the following options to `createRouter` in packages/backend/src/plugins/auth.ts +providerFactories: { + ghe: providers.github.create(), +}, +``` diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index cbd24867b7..583139cff1 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -46,11 +46,18 @@ The Microsoft provider is a structure with three configuration keys: - `clientSecret`: Secret, found on App Registration > Certificates & secrets - `tenantId`: Directory (tenant) ID, found on App Registration > Overview -In order to finish signing a user in from Azure, the Backstage backend must -fetch their information from graph.microsoft.com (as seen in [this source -code](https://github.com/seanfisher/passport-microsoft/blob/0456aa9bce05579c18e77f51330176eb26373658/lib/strategy.js#L93-L95)), -so ensure that your Backstage backend has connectivity to this host. -Otherwise users may see an `Authentication failed, failed to fetch user profile` error when they attempt to log in. +## Outbound Network Access + +If your environment has restrictions on outgoing access (e.g. through +firewall rules), make sure your Backstage backend has access to the following +hosts: + +- `login.microsoftonline.com`, to get and exchange authorization codes and access + tokens +- `graph.microsoft.com`, to fetch user profile information (as seen + in [this source + code](https://github.com/seanfisher/passport-microsoft/blob/0456aa9bce05579c18e77f51330176eb26373658/lib/strategy.js#L93-L95)). + If this host is unreachable, users may see an `Authentication failed, failed to fetch user profile` error when they attempt to log in. ## Adding the provider to the Backstage frontend diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index 04a509d316..88c22afb9b 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -34,8 +34,8 @@ more efficient caching of dependencies on the host, where a single change won't bust the entire cache. The required steps in the host build are to install dependencies with -`yarn install`, generate type definitions using `yarn tsc`, and build all -packages with `yarn build`. +`yarn install`, generate type definitions using `yarn tsc`, and build the backend +package with `yarn build:backend`. > NOTE: If you created your app prior to 2021-02-18, follow the > [migration step](https://github.com/backstage/backstage/releases/tag/release-2021-02-18) @@ -49,8 +49,8 @@ yarn install --frozen-lockfile # tsc outputs type definitions to dist-types/ in the repo root, which are then consumed by the build yarn tsc -# Build all packages and in the end bundle them all up into the packages/backend/dist folder. -yarn build +# Build the backend, which bundles it all up into the packages/backend/dist folder. +yarn build:backend ``` Once the host build is complete, we are ready to build our image. The following @@ -108,10 +108,13 @@ root of the repo to speed up the build by reducing build context size: ```text .git +.yarn/cache +.yarn/install-state.gz node_modules packages/*/src packages/*/node_modules plugins +*.local.yaml ``` With the project built and the `.dockerignore` and `Dockerfile` in place, we are @@ -272,6 +275,11 @@ browser at `http://localhost:7007` ## Separate Frontend +> NOTE: This is an optional step, and you will lose out on the features of the +> `@backstage/plugin-app-backend` plugin. Most notably the frontend configuration +> will no longer be injected by the backend, you will instead need to use the +> correct configuration when building the frontend bundle. + It is sometimes desirable to serve the frontend separately from the backend, either from a separate image or for example a static file serving provider. The first step in doing so is to remove the `app-backend` plugin from the backend diff --git a/docs/deployment/k8s.md b/docs/deployment/k8s.md index a80ba1816e..b0218c8657 100644 --- a/docs/deployment/k8s.md +++ b/docs/deployment/k8s.md @@ -256,7 +256,7 @@ bash-5.1# exit The database pod is running, but how does another pod connect to it? -Kubernetes pods are transient - they can be killed, restarted, or created +Kubernetes pods are transient - they can be stopped, restarted, or created dynamically. Therefore we don't want to try to connect to pods directly, but rather create a Kubernetes Service. Services keep track of pods and direct traffic to the right place. diff --git a/docs/dls/component-design-guidelines.md b/docs/dls/component-design-guidelines.md new file mode 100644 index 0000000000..65eef4c057 --- /dev/null +++ b/docs/dls/component-design-guidelines.md @@ -0,0 +1,108 @@ +--- +id: component-design-guidelines +title: Component Design Guidelines +description: Documentation on Design +--- + +Be it a new component contribution, or plugin specific components, you'll want +to follow these guidelines. We'll cover the three main subjects that define the +general look and feel of your components, all of which build on top of the +Material-UI theme features: + +- Layout +- Color palette +- Typography + +## 🏗️ Layout + +Layout refers to how you organize or stack content. Whenever possible, we want +to use Backstage's components (check the [Storybook][1] for a list and demo) +first, and otherwise fall back to Material-UI components (check the [MUI docs][2]). + +If none of these fit your layout needs, then you can build your own components. +However, using HTML+CSS directly is not recommended; it's better to use MUI +layout components to make your layout theme aware, meaning if someone changes +the theme, your layout would react to those changes without requiring updates +to your code. + +Specifically you want to look at these components that make use of the +`theme.spacing()` function for margins, paddings and positions, as well as +color palette and typography: + +- [Container][3] mostly at page level +- [Box][4] like a div that can be customized a lot +- [Grid][5] for flexible grid layouts +- [Paper][6] The base of a card, like it's background & padding on the borders +- [Card][7] Card with support for title, description, buttons, images... + +## Color palette + +If you're using an existing component and want to tweak the colors it uses in +general in the whole application, you can use a [Custom Theme][10] to override +specific styles for that component, that includes paddings, margins and colors. + +However when making a component from scratch you'll need to reference the theme +as much as possible, make sure to use the theme's color palette. Most Backstage +components and all MUI components should use the theme's color palette by default, +so unless you need explicit control on the color of a component (say when the +component was designed to use the primary color but you want to use the +secondary color instead), then the easiest way to access the color palette is +to [Override the Component Styles][11] as suggested by Backstage. + +It's not a very common use case to override a theme color in a MUI component +but let's say you have a custom Sidebar component with a Paper component that +highlights its content with a different color for a side menu or something +(usually you use the elevation, but maybe the designer wanted a colorful app). +You can use the theme like this: + +```tsx +import { makeStyles, Paper } from '@material-ui/core'; + +const useStyles = makeStyles((theme: Theme) => ({ + sidebarPaper: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + }, +})); + +export function Sidebar({ children }) { + const { sidebarPaper } = useStyles(); + return {children}; +} +``` + +Here is a link to the [Default Palette values][8] you can use, the tokens will +be the same, what changes are the colors associated with those depending on your +app theme color palette, there's also a [Default Theme Explorer][12] to look +which tokens you can use as reference from the compiled theme. + +## Typography + +Most of the time the components from MUI will use the `` component +which will use the theme's typography properties like font family, size, weight +and appropriate color from the palette for the context of that component. This applies for example to +buttons that use white font color for contained buttons, or the respective color +passed on via props when not outlined for proper contrast (buttons in dark +theme adapt properly by using a dark font instead of white). + +However for those cases where the parent component of the content doesn't handle +the text, like when the parent component is a layout one, you use typography +component instead of the HTML counterparts, usually used for titles and +paragraphs but it is valid for any type of text. + +Check the [Typography docs][9] for information on how to install, use, +customize semantic elements and specially the recommendations about +accessibility. + +[1]: http://backstage.io/storybook +[2]: https://v4.mui.com/getting-started/supported-components/ +[3]: https://v4.mui.com/components/container/ +[4]: https://v4.mui.com/components/box/ +[5]: https://v4.mui.com/components/grid/ +[6]: https://v4.mui.com/components/paper/ +[7]: https://v4.mui.com/components/cards/ +[8]: https://v4.mui.com/customization/palette/#default-values +[9]: https://v4.mui.com/customization/typography/ +[10]: https://backstage.io/docs/getting-started/app-custom-theme +[11]: https://backstage.io/docs/getting-started/app-custom-theme#overriding-backstage-and-material-ui-components-styles +[12]: https://v4.mui.com/customization/default-theme/#explore diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index 5b4e2fa716..616991123d 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -102,7 +102,7 @@ If either existing don't work for your use-case, it is possible to implement a custom [KubernetesClustersSupplier](https://backstage.io/docs/reference/plugin-kubernetes-backend.kubernetesclusterssupplier). -Change the following in `packages/backend/src/plugin/kubernetes.ts`: +Change the following in `packages/backend/src/plugins/kubernetes.ts`: ```diff -import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 71b9c276d9..e13401a8e3 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -68,7 +68,7 @@ export const searchPage = ( switch (result.type) { case 'software-catalog': return ( - { It's good practice for search results to highlight information that was used to return it in the first place! The code below highlights how you might specify a -custom result item component, using the `` component as +custom result item component, using the `` component as an example: ```tsx {7-13} @@ -265,7 +265,7 @@ an example: switch (result.type) { case 'software-catalog': return ( - ``` +> For more advanced customization of the Search frontend, also see how to guides such as [How to implement your own Search API](./how-to-guides.md#how-to-implement-your-own-search-api) and [How to customize search results highlighting styling](./how-to-guides.md#how-to-customize-search-results-highlighting-styling) + ### Backend Backstage Search isn't a search engine itself, rather, it provides an interface @@ -380,3 +382,5 @@ indexBuilder.addCollator({ }), }); ``` + +> For more advanced customization of the Search backend, also see how to guides such as [How to index TechDocs documents](./how-to-guides.md#how-to-index-techdocs-documents) and [How to limit what can be searched in the Software Catalog](./how-to-guides.md#how-to-limit-what-can-be-searched-in-the-software-catalog) diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 01d85b8aa2..5cc2ab5f06 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -171,6 +171,6 @@ const highlightOverride = { }; ``` -[obj-mode]: https://nodejs.org/docs/latest-v14.x/api/stream.html#stream_object_mode -[read-stream]: https://nodejs.org/docs/latest-v14.x/api/stream.html#stream_readable_streams +[obj-mode]: https://nodejs.org/dist/latest-v16.x/docs/api/stream.html#stream_object_mode +[read-stream]: https://nodejs.org/dist/latest-v16.x/docs/api/stream.html#readable-streams [async-gen]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of#iterating_over_async_generators diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 81e3604ffe..27b26659cf 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -360,7 +360,7 @@ Fields of a link are: _NOTE_: The `icon` field value is meant to be a semantic key that will map to a specific icon that may be provided by an icon library (e.g. `material-ui` icons). These keys should be a sequence of `[a-z0-9A-Z]`, possibly separated by -one of `[-_.]`. Backstage may support some basic icons out of the box, but the +one of `[-_.]`. Backstage may support some basic icons out of the box such as those [defined in app-defaults](https://github.com/backstage/backstage/blob/master/packages/app-defaults/src/defaults/icons.tsx), but the Backstage integrator will ultimately be left to provide the appropriate icon component mappings. A generic fallback icon would be provided if a mapping cannot be resolved. diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index 40f917e2f3..66813cc4d2 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -159,7 +159,12 @@ Check out the numbered markings - let's go through them one by one. the outcome of that. This example issues a `fetch` to the right service and issues a full refresh of its entity bucket based on that. 5. The method translates the foreign data model to the native `Entity` form, as - expected by the catalog. + expected by the catalog. The `Entity` must include the + `backstage.io/managed-by-location` and + `backstage.io/managed-by-origin-location annotations`; otherwise, it will not + appear in the Catalog and will generate warning logs. The + [Well-known Annotations](./well-known-annotations.md#backstageiomanaged-by-location) + documentation has guidance on what values to use for these. 6. Finally, we issue a "mutation" to the catalog. This persists the entities in our own bucket, along with an optional `locationKey` that's used for conflict checks. But this is a bigger topic - mutations warrant their own explanatory @@ -261,6 +266,129 @@ the `connect` call has been made to the provider. Start up the backend - it should now start reading from the previously registered location and you'll see your entities start to appear in Backstage. +### Example User Entity Provider + +If you have a 3rd party entity provider such as an internal HR system that you wish to use you are not limited to using our entity providers, (or simply wish to add to existing entity providers with your own data). + +We can create an entity provider to read entities that are based off that provider. + +We create a basic entity provider as shown above. In the example below we might want to extract our users from an HR system, I am assuming the HR system already has the slackUserId to get that information please see the [Slack Api](https://api.slack.com/methods). + +```typescript +import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, +} from '@backstage/catalog-model' +import { + EntityProvider, + EntityProviderConnection, +} from '@backstage/plugin-catalog-backend' +import { WebClient } from '@slack/web-api' +import {kebabCase} from 'lodash' + +interface Staff { + displayName: string + slackUserId: string + jobTitle: string + photoUrl: string + address: string + email:string +} + +export class UserEntityProvider implements EntityProvider { + private readonly getStaffUrl: string + protected readonly slackTeam: string + protected readonly slackToken: string + protected connection?: EntityProviderConnection + + static fromConfig(config: Config, options: { logger: Logger }) { + const getStaffUrl = config.getString('staff.url') + const slackToken = config.getString('slack.token') + const slackTeam = config.getString('slack.team') + return new UserEntityProvider({ + ...options, + getStaffUrl, + slackToken, + slackTeam, + }) + } + + private constructor(options: { + getStaffUrl: string + slackToken: string + slackTeam: string + }) { + this.getStaffUrl = options.getStaffUrl + this.slackToken = options.slackToken + this.slackTeam = options.slackTeam + } + + async getAllStaff(): Promise{ + await return axios.get(this.getStaffUrl) + } + + public async connect(connection: EntityProviderConnection): Promise { + this.connection = connection + } + + async run(): Promise { + if (!this.connection) { + throw new Error('User Connection Not initialized') + } + + const userResources: UserEntity[] = [] + const staff = await this.getAllStaff() + + for (const user of staff) { + // we can add any links here in this case it would be adding a slack link to the users so you can directly slack them. + const links = + user.slackUserId != null && user.slackUserId.length > 0 + ? [ + { + url: `slack://user?team=${this.slackTeam}&id=${user.slackUserId}`, + title: 'Slack', + icon: 'message', + }, + ] + : undefined + const userEntity: UserEntity = { + kind: 'User', + apiVersion: 'backstage.io/v1alpha1', + metadata: { + annotations: { + [ANNOTATION_LOCATION]: 'hr-user-https://www.hrurl.com/', + [ANNOTATION_ORIGIN_LOCATION]: 'hr-user-https://www.hrurl.com/', + }, + links, + // name of the entity + name: kebabCase(user.displayName), + // name for display purposes could be anything including email + title: user.displayName, + }, + spec: { + profile: { + displayName: user.displayName, + email: user.email, + picture: user.photoUrl, + }, + memberOf: [], + }, + } + + userResources.push(userEntity) + } + + await this.connection.applyMutation({ + type: 'full', + entities: userResources.map((entity) => ({ + entity, + locationKey: 'hr-user-https://www.hrurl.com/', + })), + }) +} + +``` + ## Custom Processors The other possible way of ingesting data into the catalog is through the use of diff --git a/docs/features/software-catalog/system-model.md b/docs/features/software-catalog/system-model.md index 8a7e0cd30a..409f3e8450 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 a whitelisted set of +other component to consume), restricted (only available to an allowlisted 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-catalog/well-known-relations.md b/docs/features/software-catalog/well-known-relations.md index 6fb7ae2fea..bfcbcb780a 100644 --- a/docs/features/software-catalog/well-known-relations.md +++ b/docs/features/software-catalog/well-known-relations.md @@ -76,6 +76,9 @@ an entity to function. It can for example be used to express that a website component needs a library component as part of its build, or that a service component uses a persistent storage resource. +This relation is commonly generated based on `spec.dependsOn` of the component +or resource in question. + ### `parentOf` and `childOf` A parent/child relation to build up a tree, used for example to describe the diff --git a/docs/features/software-templates/input-examples.md b/docs/features/software-templates/input-examples.md new file mode 100644 index 0000000000..c44ee39269 --- /dev/null +++ b/docs/features/software-templates/input-examples.md @@ -0,0 +1,195 @@ +--- +id: input-examples +title: Input Examples +description: Some examples to use in your template +--- + +All the examples on this page you can test using _create/edit_ from your Backstage installation. + +It is important to remember that all examples are based on [react-jsonschema-form](https://rjsf-team.github.io/react-jsonschema-form/). + +## Simple text input + +### Simple input with basic validations + +```yaml +parameters: + - title: Fill in some steps + properties: + name: + title: Simple text input + type: string + description: Description about input + maxLength: 8 + pattern: '^([a-zA-Z][a-zA-Z0-9]*)(-[a-zA-Z0-9]+)*$' + ui:autofocus: true + ui:help: 'Hint: additional description...' +``` + +### Multi line text input + +```yaml +parameters: + - title: Fill in some steps + properties: + multiline: + title: Text area input + type: string + description: Insert your multi line string + ui:widget: textarea + ui:options: + rows: 10 + ui:help: 'Hint: Make it strong!' + ui:placeholder: | + apiVersion: backstage.io/v1alpha1 + kind: Component + metadata: + name: backstage + spec: + type: library + owner: CNCF + lifecycle: experimental +``` + +## Arrays options + +### Array with custom titles + +```yaml +parameters: + - title: Fill in some steps + properties: + volume_type: + title: Volume Type + type: string + description: The volume type to be used + default: gp2 + enum: + - gp2 + - gp3 + - io1 + - io2 + - sc1 + - st1 + - standard + enumNames: + - 'General Purpose SSD (gp2)' + - 'General Purpose SSD (gp3)' + - 'Provisioned IOPS (io1)' + - 'Provisioned IOPS (io2)' + - 'Cold HDD (sc1)' + - 'Throughput Optimized HDD (st1)' + - 'Magnetic (standard)' +``` + +### A multiple choices list + +```yaml +parameters: + - title: Fill in some steps + properties: + name: + title: Select environments + type: array + items: + type: string + enum: + - production + - staging + - development + uniqueItems: true + ui:widget: checkboxes +``` + +### Array with another types + +```yaml +parameters: + - title: Fill in some steps + properties: + arrayObjects: + title: Array with custom objects + type: array + ui:options: + addable: false + orderable: false + removable: false + items: + type: object + properties: + array: + title: Array string with default value + type: string + default: value3 + enum: + - value1 + - value2 + - value3 + flag: + title: Boolean flag + type: boolean + ui:widget: radio + someInput: + title: Simple text input + type: string +``` + +## Boolean options + +### Boolean + +```yaml +parameters: + - title: Fill in some steps + properties: + name: + title: Checkbox boolean + type: boolean +``` + +### Boolean Yes or No options + +```yaml +parameters: + - title: Fill in some steps + properties: + name: + title: Yes or No options + type: boolean + ui:widget: radio +``` + +### Boolean multiple options + +```yaml +parameters: + - title: Fill in some steps + properties: + name: + title: Select features + type: array + items: + type: boolean + enum: + - 'Enable scraping' + - 'Enable HPA' + - 'Enable cache' + uniqueItems: true + ui:widget: checkboxes +``` + +## Use parameters as condition in steps + +```yaml +- name: Only development environments + if: ${{ parameters.environment === "staging" and parameters.environment === "development" }} + action: debug:log + input: + message: 'development step' + +- name: Only production environments + if: ${{ parameters.environment === "prod" or parameters.environment === "production" }} + action: debug:log + input: + message: 'production step' +``` diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index b4c4d2d4ce..1d28bd5db9 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -181,3 +181,102 @@ const CustomFieldExtension = scaffolderPlugin.provide( }) ); ``` + +## Previewing Custom Field Extensions + +You can preview custom field extensions you write in the Backstage UI using the Custom Field Explorer +(accessible via the `/create/edit` route by default): + +![Custom Field Explorer](../../assets/software-templates/custom-field-explorer.png) + +In order to make your new custom field extension available in the explorer you will have to define a +JSON schema that describes the input/output types on your field like in the following example: + +```tsx +//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx +export const MyCustomExtensionWithOptionsSchema = { + uiOptions: { + type: 'object', + properties: { + focused: { + description: 'Whether to focus this field', + type: 'boolean', + }, + }, + }, + returnValue: { type: 'string' }, +}; + +export const MyCustomExtensionWithOptions = ({ + onChange, + rawErrors, + required, + formData, +}: FieldExtensionComponentProps) => { + return ( + 0 && !formData} + onChange={onChange} + focused={focused} + /> + ); +}; +``` + +```tsx +// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.ts +... +import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema } from './MyCustomExtensionWithOptions'; + +export const MyCustomFieldWithOptionsExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + name: 'MyCustomExtensionWithOptions', + component: MyCustomExtensionWithOptions, + schema: MyCustomExtensionWithOptionsSchema, + }), +); +``` + +We recommend using a library like [zod](https://github.com/colinhacks/zod) to define your schema +and the provided `makeFieldSchemaFromZod` helper utility function to generate both the JSON schema +and type for your field props to preventing having to duplicate the definitions: + +```tsx +//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx +... +import { z } from 'zod'; +import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder'; + +const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod( + z.string(), + z.object({ + focused: z + .boolean() + .optional() + .describe('Whether to focus this field'), + }), +); + +export const MyCustomExtensionWithOptionsSchema = MyCustomExtensionWithOptionsFieldSchema.schema; + +type MyCustomExtensionWithOptionsProps = typeof MyCustomExtensionWithOptionsFieldSchema.type; + +export const MyCustomExtensionWithOptions = ({ + onChange, + rawErrors, + required, + formData, +}: MyCustomExtensionWithOptionsProps) => { + return ( + 0 && !formData} + onChange={onChange} + focused={focused} + /> + ); +}; +``` diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index dc89f1718b..06eb83dfca 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -235,7 +235,7 @@ details. You need to make sure that your environment is able to authenticate with the target cloud provider. `techdocs-cli` uses the official Node.js clients provided -by AWS (v2), Google Cloud and Azure. You can authenticate using environment +by AWS (v3), Google Cloud and Azure. You can authenticate using environment variables and/or by other means (`~/.aws/credentials`, `~/.config/gcloud` etc.) Refer to the Authentication section of the following documentation depending diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index 8a54d76ed0..4e806296e5 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -108,8 +108,8 @@ techdocs: # (Optional) An API key is required to write to a storage bucket. # If not set, environment variables or aws config file will be used to authenticate. - # https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html - # https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html + # https://www.npmjs.com/package/@aws-sdk/credential-provider-node + # https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html credentials: accessKeyId: ${TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL} secretAccessKey: ${TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL} @@ -121,7 +121,7 @@ techdocs: # (Optional) Endpoint URI to send requests to. # If not set, the default endpoint is built from the configured region. - # https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property + # https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/s3clientconfig.html#endpoint endpoint: ${AWS_ENDPOINT} # (Optional) Whether to use path style URLs when communicating with S3. diff --git a/docs/features/techdocs/configuring-ci-cd.md b/docs/features/techdocs/configuring-ci-cd.md index 0f8044e9c9..75800c48ca 100644 --- a/docs/features/techdocs/configuring-ci-cd.md +++ b/docs/features/techdocs/configuring-ci-cd.md @@ -141,14 +141,16 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - - uses: actions/setup-node@v2 - - uses: actions/setup-python@v2 + - uses: actions/setup-node@v3 + - uses: actions/setup-python@v4 + with: + python-version: '3.9' # the 2 steps below can be removed if you aren't using plantuml in your documentation - name: setup java - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: distribution: 'zulu' java-version: '11' diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index 66b4f09596..3ea57edc6b 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -219,22 +219,19 @@ If the environment variables - `AWS_REGION` are set and can be used to access the bucket you created in step 2, they will be -used by the AWS SDK V2 Node.js client for authentication. -[Refer to the official documentation for loading credentials in Node.js from environment variables](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html). +used by the AWS SDK V3 Node.js client for authentication. +[Refer to the official documentation for loading credentials in Node.js from environment variables](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html). If the environment variables are missing, the AWS SDK tries to read the `~/.aws/credentials` file for credentials. -[Refer to the official documentation.](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html) +[Refer to the official documentation.](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html) -If you are using Amazon EC2 instance to deploy Backstage, you do not need to -obtain the access keys separately. They can be made available in the environment -automatically by defining appropriate IAM role with access to the bucket. Read -more in +If you are deploying Backstage to Amazon EC2, Amazon ECS, or Amazon EKS, you do +not need to obtain the access keys separately. They can be made available in the +environment automatically by defining appropriate IAM role with access to the +bucket. Read more in the [official AWS documentation for using IAM roles.](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html#use-roles). -The AWS Region of the bucket is optional since TechDocs uses AWS SDK V2 and not -V3. - **4b. Authentication using app-config.yaml** AWS credentials and region can be provided to the AWS SDK via `app-config.yaml`. @@ -254,7 +251,7 @@ techdocs: ``` Refer to the -[official AWS documentation for obtaining the credentials](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/getting-your-credentials.html). +[official AWS documentation for obtaining the credentials](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html). **4c. Authentication using an assumed role** Users with multiple AWS accounts may want to use a role for S3 storage that is in a different AWS account. Using diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index 146f160d71..5ec0d6eeab 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -393,6 +393,98 @@ You can add more icons, if the [default icons](https://github.com/backstage/back Note: If the icon is not available as one of the default icons or one you've added then it will fall back to Material UI's `LanguageIcon` +## Custom Sidebar + +As you've seen there are many ways that you can customize your Backstage app. The following section will show you how you can customize the sidebar. + +### Sidebar Sub-menu + +For this example we'll show you how you can expand the sidebar with a sub-menu: + +1. Open the `Root.tsx` file located in `packages/app/src/components/Root` as this is where the sidebar code lives +2. Then we want to add the following imports for the icons: + + ```ts + import ApiIcon from '@material-ui/icons/Extension'; + import ComponentIcon from '@material-ui/icons/Memory'; + import DomainIcon from '@material-ui/icons/Apartment'; + import ResourceIcon from '@material-ui/icons/Work'; + import SystemIcon from '@material-ui/icons/Category'; + import UserIcon from '@material-ui/icons/Person'; + ``` + +3. Then update the `@backstage/core-components` import like this: + + ```diff + import { + Sidebar, + sidebarConfig, + SidebarDivider, + SidebarGroup, + SidebarItem, + SidebarPage, + SidebarScrollWrapper, + SidebarSpace, + useSidebarOpenState, + Link, + + GroupIcon, + + SidebarSubmenu, + + SidebarSubmenuItem, + } from '@backstage/core-components'; + ``` + +4. Finally replace `` with this: + + ```ts + + + + + + + + + + + + + + ``` + +When you startup your Backstage app and hover over the Home option on the sidebar you'll now see a nice sub-menu appear with links to the various Kinds in your Catalog. It would look like this: + +![Sidebar sub-menu example](./../assets/getting-started/sidebar-submenu-example.png) + +You can see more ways to use this in the [Storybook Sidebar examples](https://backstage.io/storybook/?path=/story/layout-sidebar--sample-scalable-sidebar) + ## Custom Homepage In addition to a custom theme, a custom logo, you can also customize the diff --git a/docs/getting-started/contributors.md b/docs/getting-started/contributors.md index 9154eae4d1..cda53f8a54 100644 --- a/docs/getting-started/contributors.md +++ b/docs/getting-started/contributors.md @@ -29,8 +29,9 @@ After you have cloned the Backstage repository, you should run the following commands once to set things up for development: ```bash -$ yarn install # fetch dependency packages - may take a while +$ cd backstage # change to root directory of project +$ yarn install # fetch dependency packages - may take a while $ yarn tsc # does a first run of type generation and checks ``` @@ -101,7 +102,8 @@ yarn workspace @backstage/plugin-api-docs start # Serve api-docs plugin only, al yarn tsc # Run typecheck, use --watch for watch mode yarn tsc:full # Run full type checking, for example without skipLibCheck, use in CI -yarn build # Build published versions of packages, depends on tsc +yarn build:backend # Build the backend package, depends on tsc +yarn build:all # Build published versions of packages, depends on tsc yarn lint # lint packages that have changed since later commit on origin/master yarn lint:all # lint all packages diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index eac76b5fdc..2d34998013 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -78,7 +78,7 @@ const routes = ( ) ``` -### 4. Update sidebar items +#### 4. Update sidebar items Let's update the route for "Home" in the Backstage sidebar to point to the new homepage. We'll also add a Sidebar item to quickly open Catalog. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 477467bb88..16e361a19f 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -34,6 +34,7 @@ guide to do a repository-based installation. - [Package manager](https://nodejs.org/en/download/package-manager/) - [Using NodeSource packages](https://github.com/nodesource/distributions/blob/master/README.md) - `yarn` [Installation](https://classic.yarnpkg.com/en/docs/install) + - You will need to use Yarn classic to create a new project, but it can then be [migrated to Yarn 3](../tutorials/yarn-migration.md) - `docker` [installation](https://docs.docker.com/engine/install/) - `git` [installation](https://github.com/git-guides/install-git) - If the system is not directly accessible over your network the following ports diff --git a/docs/getting-started/running-backstage-locally.md b/docs/getting-started/running-backstage-locally.md index 7cc6f6be6a..e7e96316f4 100644 --- a/docs/getting-started/running-backstage-locally.md +++ b/docs/getting-started/running-backstage-locally.md @@ -21,12 +21,12 @@ This is made easy with a version manager such as # Installing current LTS release nvm install --lts > Installing latest LTS version. -> Downloading and installing node v14.15.1... -> Now using node v14.15.1 (npm v6.14.8) +> Downloading and installing node v16.16.0... +> Now using node v16.16.0 (npm v8.11.0) # Checking your version node --version -> v14.15.1 +> v16.16.0 ``` - Yarn @@ -41,47 +41,38 @@ locally to use features like Software Templates and TechDocs. Please refer to the [installation instructions for Docker](https://docs.docker.com/engine/install/). -## Clone and Build +## Clone and Install To get up and running with a local Backstage to evaluate it, let's clone it off -of GitHub and run an initial build. +of GitHub and run an initial install. ```bash # Start from your local development folder -git clone --depth 1 git@github.com:backstage/backstage.git +git clone --depth 1 https://github.com/backstage/backstage.git cd backstage -# Fetch our dependencies and run an initial build +# Install our dependencies yarn install -yarn tsc -yarn build ``` Phew! Now you have a local repository that's ready to run and to add any open source contributions into. We are now going to launch two things: an example Backstage frontend app, and an -example Backstage backend that the frontend talks to. You are going to need two -terminal windows, both starting from the Backstage project root. - -In the first window, run +example Backstage backend that the frontend talks to. These can both be launched +through the following command: ```bash -# From your Backstage root directory -yarn --cwd packages/backend start +# From your Backstage root directory, launches both the frontend and backend +yarn dev ``` -That starts up a backend instance on port 7007. +If you prefer to run the frontend and backend separately, you can instead use `yarn start` +and `yarn start-backend` in two separate terminal windows. -In the other window, we will then launch the frontend. This command is run from -the project root, not inside the backend directory. - -```bash -yarn start -``` - -That starts up the frontend on port 3000, and should automatically open a -browser window showing it. +Which ever way you choose, you will now have a backend instance running on port 7007, +and the frontend running on port 3000. A browser window should also automatically open, +showing the frontend. ## Authentication diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index 99c517b568..acb43db182 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -101,7 +101,7 @@ To grant the managed identity the same permissions as mentioned in _App Registra ## Filtering imported Users and Groups By default, the plugin will import all users and groups from your directory. -This can be customized through filters and search queries. +This can be customized through [filters](https://learn.microsoft.com/en-us/graph/filter-query-parameter) and [search](https://learn.microsoft.com/en-us/graph/search-query-parameter) queries. ### Groups @@ -228,6 +228,17 @@ export async function myOrganizationTransformer( ## Troubleshooting +### No data + +First check your logs for the message `Reading msgraph users and groups`. +If you don't see this, check you've registered the provider, and that the schedule is valid + +If you see a log entry `Read 0 msgraph users and 0 msgraph groups`, check your search and filter arguments. + +If you see the start message (`Reading msgraph users and groups`) but no end message (`Read X msgraph users and Y msgraph groups`), then it is likely the job is taking a long time due to a large volume of data. +The default behavior is to import all users and groups, which is often more data than needed. +Try importing a smaller set of data (e.g. `filter: displayName eq 'John Smith'`). + ### Authentication / Token Errors See [Troubleshooting Azure Identity Authentication Issues](https://aka.ms/azsdk/js/identity/troubleshoot) diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index 9b9bc4ecde..f6ceab9ff7 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -24,10 +24,12 @@ package. yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-bitbucket-cloud ``` +### Installation without Events Support + And then add the entity provider to your catalog builder: ```diff - // In packages/backend/src/plugins/catalog.ts +// packages/backend/src/plugins/catalog.ts + import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud'; export default async function createPlugin( @@ -37,11 +39,7 @@ And then add the entity provider to your catalog builder: + builder.addEntityProvider( + BitbucketCloudEntityProvider.fromConfig(env.config, { + logger: env.logger, -+ // optional: alternatively, configure via app-config.yaml -+ schedule: env.scheduler.createScheduledTaskRunner({ -+ frequency: { minutes: 30 }, -+ timeout: { minutes: 3 }, -+ }), ++ scheduler: env.scheduler, + }), + ); @@ -49,6 +47,62 @@ And then add the entity provider to your catalog builder: } ``` +Alternatively to the config-based schedule, you can use + +```diff +- scheduler: env.scheduler, ++ schedule: env.scheduler.createScheduledTaskRunner({ ++ frequency: { minutes: 30 }, ++ timeout: { minutes: 3 }, ++ }), +``` + +### Installation with Events Support + +Please follow the installation instructions at + +- https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md +- https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-bitbucket-cloud/README.md + +Additionally, you need to decide how you want to receive events from external sources like + +- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) +- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) + +Set up your provider + +```diff +// packages/backend/src/plugins/catalogEventBasedProviders.ts ++import { CatalogClient } from '@backstage/catalog-client'; ++import { BitbucketCloudEntityProvider } from '@backstage/plugin-catalog-backend-module-bitbucket-cloud'; + import { EntityProvider } from '@backstage/plugin-catalog-node'; + import { EventSubscriber } from '@backstage/plugin-events-node'; + import { PluginEnvironment } from '../types'; + + export default async function createCatalogEventBasedProviders( +- _: PluginEnvironment, ++ env: PluginEnvironment, + ): Promise> { + const providers: Array< + (EntityProvider & EventSubscriber) | Array + > = []; +- // add your event-based entity providers here ++ providers.push( ++ BitbucketCloudEntityProvider.fromConfig(env.config, { ++ catalogApi: new CatalogClient({ discoveryApi: env.discovery }), ++ logger: env.logger, ++ scheduler: env.scheduler, ++ tokenManager: env.tokenManager, ++ }), ++ ); + return providers.flat(); + } +``` + +**Attention:** +`catalogApi` and `tokenManager` are required at this variant +compared to the one without events support. + ## Configuration To use the entity provider, you'll need a [Bitbucket Cloud integration set up](locations.md). diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index 4f032901a9..bbbe86a834 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -14,7 +14,7 @@ organization and register entities matching the configured path. This can be useful as an alternative to static locations or manually adding things to the catalog. This is the preferred method for ingesting entities into the catalog. -## Installation +## Installation without Events Support You will have to add the provider in the catalog initialization code of your backend. They are not installed by default, therefore you have to add a @@ -53,6 +53,52 @@ And then add the entity provider to your catalog builder: } ``` +## Installation with Events Support + +Please follow the installation instructions at + +- https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md +- https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md + +Additionally, you need to decide how you want to receive events from external sources like + +- [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) +- [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) + +Set up your provider + +```diff +// packages/backend/src/plugins/catalogEventBasedProviders.ts ++import { GithubEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; + import { EntityProvider } from '@backstage/plugin-catalog-node'; + import { EventSubscriber } from '@backstage/plugin-events-node'; + import { PluginEnvironment } from '../types'; + export default async function createCatalogEventBasedProviders( +- _: PluginEnvironment, ++ env: PluginEnvironment, + ): Promise> { + const providers: Array< + (EntityProvider & EventSubscriber) | Array + > = []; +- // add your event-based entity providers here ++ providers.push( ++ GithubEntityProvider.fromConfig(env.config, { ++ logger: env.logger, ++ // optional: alternatively, use scheduler with schedule defined in app-config.yaml ++ schedule: env.scheduler.createScheduledTaskRunner({ ++ frequency: { minutes: 30 }, ++ timeout: { minutes: 3 }, ++ }), ++ // optional: alternatively, use schedule ++ scheduler: env.scheduler, ++ }), ++ ); + return providers.flat(); + } +``` + +You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks). The webhook will need to be configured to forward `push` events. + ## Configuration To use the discovery provider, you'll need a GitHub integration diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index 5482733a58..9e3c450adc 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -38,7 +38,7 @@ schedule it: ```diff // packages/backend/src/plugins/catalog.ts -+import { GitHubOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; ++import { GithubOrgEntityProvider } from '@backstage/plugin-catalog-backend-module-github'; export default async function createPlugin( env: PluginEnvironment, @@ -48,7 +48,7 @@ schedule it: + // The org URL below needs to match a configured integrations.github entry + // specified in your app-config. + builder.addEntityProvider( -+ GitHubOrgEntityProvider.fromConfig(env.config, { ++ GithubOrgEntityProvider.fromConfig(env.config, { + id: 'production', + orgUrl: 'https://github.com/backstage', + logger: env.logger, @@ -97,6 +97,66 @@ that must be approved first before the changes are applied.** ![email](../../assets/integrations/github/email.png) +### Custom Transformers + +You can inject your own transformation logic to help map from GH API responses +into backstage entities. You can do this on the user and team requests to +enable you to do further processing or updates to the entities. + +To enable this you pass a function into the `GitHubOrgEntityProvider`. You can +pass a `UserTransformer`, `TeamTransformer` or both. The function is invoked +for each item (user or team) that is returned from the API. You can either +return an Entity (User or Group) or `undefined` if you do not want to import +that item. + +There is also a `defaultUserTransformer` and `defaultOrganizationTeamTransformer`. +You could use these and simply decorate the response from the default +transformation if you only need to change a few properties. + +### Resolving GitHub users via organization email + +When you authenticate users you should resolve them to an entity within the +catalog. Often the authentication you use could be a corporate SSO system that +provides you with email as a key. To enable you to find and resolve GitHub users +it's useful to also import the private domain verified emails into the User +entity in backstage. + +The integration attempts to return `organizationVerifiedDomainEmails` from the +GitHub API and makes this available as part of the object passed to +`UserTransformer`. The GitHub API will only return emails that use a domain +that's a verified domain for your GitHub Org. It also relies on the user having +configured such an email in their own account. The API will only return these +values when using GitHub App authentication and with the correct app permission +allowing access to emails. + +You can decorate the `defaultUserTransformer` to replace the org email in the +returned identity. + +```typescript +async (user, ctx): Promise => { + const entity = await defaultUserTransformer(user, ctx); + + if (entity && user.organizationVerifiedDomainEmails) { + entity.spec.profile!.email = user.organizationVerifiedDomainEmails[0] || ''; + } + + return entity; +}, +``` + +Once you have imported the emails you can resolve users in your sign-in in +resolver using the catalog entity search via email + +```typescript +// packages/backend/src/plugins/auth.ts +ctx.signInWithCatalogUser({ + filter: { + kind: ['User'], + 'spec.profile.email': email as string, + }, +}); +``` + ## Using a Processor instead of a Provider An alternative to using the Provider for ingesting organizational entities is to diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index 3c9417268b..3ba3f4ba0b 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -205,7 +205,7 @@ A typical plugin consists of up to five packages, two frontend ones, two backend, and one isomorphic package. All packages within the plugin must share a common prefix, typically of the form `@/plugin-`, but alternatives like `backstage-plugin-` or -`@scope/backstage-plugin-` are also valid. Along with this prefix, +`@/backstage-plugin-` are also valid. Along with this prefix, each of the packages have their own unique suffix that denotes their role. In addition to these five plugin packages it's also possible for a plugin to have additional frontend and backend modules that can be installed to enable optional @@ -249,7 +249,7 @@ however likely to change in the future. The common packages are the packages effectively depended on by all other pages. This is a much smaller set of packages but they are also very pervasive. Because the common packages are isomorphic and must execute both in the frontend and -backend, they are never allowed to depend on any of the frontend of backend +backend, they are never allowed to depend on any of the frontend or backend packages. The Backstage CLI is in a category of its own and is depended on by virtually @@ -260,7 +260,7 @@ development dependency only. It can sometimes be difficult to decide where to place your plugin code. For example should it go directly in the `-backend` plugin package or in the `-node` package? -As a rule of thumb you should try to keep the exposure of your code as low +As a general guideline you should try to keep the exposure of your code as low as possible. If it doesn't need to be public API, it's best to avoid. If you don't need it to be used by other plugins, then keep it directly in the plugin packages. diff --git a/docs/overview/versioning-policy.md b/docs/overview/versioning-policy.md index 885a599ed8..10ffc82188 100644 --- a/docs/overview/versioning-policy.md +++ b/docs/overview/versioning-policy.md @@ -99,7 +99,7 @@ This versioning is completely decoupled from the Backstage release versioning, meaning you might for example have `@backstage/core-plugin-api` version `3.1.4` be part of the `1.12` Backstage release. -Following versioning policy applies to all packages: +The following versioning policy applies to all packages: - Breaking changes are noted in the changelog, and documentation is updated. - Breaking changes are prefixed with `**BREAKING**: ` in the changelog. @@ -120,6 +120,25 @@ For packages at version `1.0.0` or above, the following policy also applies: changes without a deprecation period, but the changes must still adhere to semver. +### Changes that are Not Considered Breaking + +There are a few changes that would typically be considered breaking changes, but +that we make exceptions for. This is both to be able to evolve the project more +rapidly, also because the alternative ends up having a bigger impact on users. + +For all Utility APIs and Backend Services that _have_ a built-in implementation, +we only consider the API stability for consumers of those interfaces. This means +that it is not considered a breaking change to break the contract for producers +of the interface. + +Changes that fall under the above rule, must be marked with +`**BREAKING PRODUCERS**:` in the changelog. + +For any case of dependency injection, it is not considered a breaking change to +add a dependency on a Utility API or Backend Service that is provided by the +framework. This includes any dependency that is provided by the +`@backstage/app-defaults` and `@backstage/backend-defaults` packages. + ### Release Stages The release stages(`@alpha`, `@beta` `@public`) refers to the @@ -144,9 +163,10 @@ following schedule for determining the [Node.js releases](https://nodejs.org/en/ - At any given point in time we support exactly two adjacent even-numbered releases of Node.js, for example v12 and v14. -- Three months before a Node.js release becomes _Active LTS_ we switch support - to that release and the previous one. This is halfway through the _Current LTS_ - cycle for that release and occurs at the end of July every year. +- Once a new Node.js release becomes _Active LTS_ we switch to support that + release and the previous one. The switch is not immediate but done as soon + as possible. You can find the Node.js version supported by each release + in the `engines` field in the root `package.json` of a new app. When we say _Supporting_ a Node.js release, that means the following: diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 46e1d354df..e04cb9c5c6 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -52,13 +52,13 @@ learn how to contribute the integration yourself! The following table summarizes events that, depending on the plugins you have installed, may be captured. -| Action | Subject | Other Notes | -| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | -| `navigate` | The URL of the page that was navigated to | | -| `click` | The text of the link that was clicked on | The `to` attribute represents the URL clicked to | -| `create` | The `name` of the software being created; if no `name` property is requested by the given Software Template, then the string `new {templateName}` is used instead. | The context holds an `entityRef`, set to the template's ref (e.g. `template:default/template-name`) | -| `search` | The search term entered in any search bar component | The context holds `searchTypes`, representing `types` constraining the search | -| `discover` | The title of the search result that was clicked on | The `value` is the result rank. A `to` attribute is also provided | +| Action | Subject | Other Notes | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `navigate` | The URL of the page that was navigated to | | +| `click` | The text of the link that was clicked on | The `to` attribute represents the URL clicked to | +| `create` | The `name` of the software being created; if no `name` property is requested by the given Software Template, then the string `new {templateName}` is used instead. | The context holds an `entityRef`, set to the template's ref (e.g. `template:default/template-name`) | +| `search` | The search term entered in any search bar component | - The context holds `searchTypes`, representing `types` constraining the search. The `value` represents the total number of search results for the query. This may not be visible if the permission framework is being used. | +| `discover` | The title of the search result that was clicked on | The `value` is the result rank. A `to` attribute is also provided | If there is an event you'd like to see captured, please [open an issue][add-event] describing the event you want to see and the questions it diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 3eddb9e1c2..65a668d391 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -51,7 +51,7 @@ terminal window, now run curl localhost:7007/carmen/health ``` -This should return `{"status":"ok"}`. Success! Press `Ctrl + c` to kill it +This should return `{"status":"ok"}`. Success! Press `Ctrl + c` to stop it again. ## Developing your Backend Plugin diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index aa7815728c..baee47c695 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -123,7 +123,7 @@ than simply letting them know they had an invalid input. > input change. This piggybacks the Scalability Principle and applies primarily to frontend -development. As a general rule of thumb, frontends should be flexible enough so +development. As a general guideline, frontends should be flexible enough so that the UX or design can change while touching the least amount of code possible. So for example, a poor unit test would verify the color of a button when it is hovered. This would be a poor unit test, because if you decide to @@ -353,7 +353,7 @@ For more information: ### Accessing `store`, `theme`, routing, browser history, etc. The Backstage application has several core providers at its root. To run your -test wrapped in a "dummy" Backstage application, you can use our utility +test wrapped in a "sample" Backstage application, you can use our utility functions: **`wrapInTestApp`** diff --git a/docs/plugins/url-reader.md b/docs/plugins/url-reader.md index 8788737f17..815674091b 100644 --- a/docs/plugins/url-reader.md +++ b/docs/plugins/url-reader.md @@ -268,7 +268,7 @@ async function main() { ``` This will be run every time you restart the backend. Note that after any change -in the URL Reader code, you need to kill the backend and restart, since the +in the URL Reader code, you need to stop the backend and restart, since the `reader` instance is memoized and does not update on hot module reloading. Also, there are a lot of unit tests written for the URL Readers, which you can make use of. diff --git a/docs/releases/v1.8.0-changelog.md b/docs/releases/v1.8.0-changelog.md new file mode 100644 index 0000000000..4b36b01abf --- /dev/null +++ b/docs/releases/v1.8.0-changelog.md @@ -0,0 +1,2837 @@ +# Release v1.8.0 + +## @backstage/backend-common@0.16.0 + +### Minor Changes + +- a7607b5413: **BREAKING CHANGE**: The `UrlReader` interface has been updated to require that `readUrl` is implemented. `readUrl` has previously been optional to implement but a warning has been logged when calling its predecessor `read`. + The `read` method is now deprecated and will be removed in a future release. + +### Patch Changes + +- 88f99b8b13: Bumped `tar` dependency to `^6.1.12` in order to ensure Node.js v18 compatibility. +- 55227712dd: Generated development HTTPS backend certificate is now checked for expiration date instead of file age. +- d05e1841ce: This patch adds GiteaURLReader to the available classes. It currently only reads single files via gitea's public repos api +- e6ced2446a: Change to using `@keyv/memcache` now that `keyv-memcache` is deprecated +- 210a3b5668: Small update to fix compatibility with newer versions of the `keyv` library +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- Updated dependencies + - @backstage/integration@1.4.0 + - @backstage/types@1.0.1 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4 + - @backstage/config-loader@1.1.6 + - @backstage/errors@1.1.3 + +## @backstage/cli@0.21.0 + +### Minor Changes + +- 7539b36748: Added a new ESLint rule that restricts imports of Link from @material-ui + + The rule can be can be overridden in the following way: + + ```diff + module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + + restrictedImports: [ + + { name: '@material-ui/core', importNames: [] }, + + { name: '@material-ui/core/Link', importNames: [] }, + + ], + }); + ``` + +- 384eaa2307: Switched `tsconfig.json` to target and support `ES2021`, in line with the bump to Node.js 16 & 18. + +### Patch Changes + +- e52d6ad861: Updated the `backstage-cli` so that the `list-deprecations` command is visible and also removed the "[EXPERIMENTAL]" tag. +- 88f99b8b13: Bumped `tar` dependency to `^6.1.12` in order to ensure Node.js v18 compatibility. +- df21bbd4ad: Removed googleAnalyticsTrackingId configSchema. +- 286d474675: Minor update to the `index.html` template that updates the comment at the end to be more accurate. +- 4c16213e7d: The built-in Jest configuration now always uses the Jest environments that are bundled with the CLI by default. This avoids a situation where Jest potentially picks up an incompatible version of the environment package from a different dependency in the project. +- 4091c73e68: Updated `@swc/core` to version 1.3.9 which fixes a `.tsx` parser bug +- 969a8444ea: Updated dependency `esbuild` to `^0.15.0`. +- 9c767e8f45: Updated dependency `@svgr/plugin-jsx` to `6.5.x`. + Updated dependency `@svgr/plugin-svgo` to `6.5.x`. + Updated dependency `@svgr/rollup` to `6.5.x`. + Updated dependency `@svgr/webpack` to `6.5.x`. +- Updated dependencies + - @backstage/release-manifests@0.0.7 + - @backstage/types@1.0.1 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4 + - @backstage/config-loader@1.1.6 + - @backstage/errors@1.1.3 + +## @backstage/core-app-api@1.2.0 + +### Minor Changes + +- 9b737e5f2e: Updated the React Router wiring to make use of the new `basename` property of the router components in React Router v6 stable. To implement this, a new optional `basename` property has been added to the `Router` app component, which can be forwarded to the concrete router implementation in order to support this new behavior. This is done by default in any app that does not have a `Router` component override. +- 127fcad26d: Deprecated the `homepage` config as the component that used it - `HomepageTimer` - has been removed and replaced by the `HeaderWorldClock` in the home plugin + +### Patch Changes + +- Updated dependencies + - @backstage/version-bridge@1.0.2 + - @backstage/core-plugin-api@1.1.0 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + +## @backstage/core-components@0.12.0 + +### Minor Changes + +- fb3733e446: **BREAKING**: Removed the `HomepageTimer` as it has been replaced by the `HeaderWorldClock` in the Home plugin and was deprecated over a year ago. + +### Patch Changes + +- 5f695c219a: Set the `searchTooltip` to "Filter" to follow how the `searchPlaceholder` is set making this more consistent +- f36127f5fe: Add optional step to SimpleStepper +- b4fb5c8ecc: MissingAnnotationEmptyState now accepts either a string or an array of strings to support multiple missing annotations. +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- 858986f6b6: Disable base path workaround in `Link` component when React Router v6 stable is used. +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/version-bridge@1.0.2 + - @backstage/core-plugin-api@1.1.0 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/core-plugin-api@1.1.0 + +### Minor Changes + +- a228f113d0: The app `Router` component now accepts an optional `basename` property. + +### Patch Changes + +- Updated dependencies + - @backstage/version-bridge@1.0.2 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + +## @backstage/integration@1.4.0 + +### Minor Changes + +- d05e1841ce: This patch brings Gitea as a valid integration: target, via the ScmIntegration interface. It adds gitea to the relevant static properties (get integration by name, get integration by type) for plugins to be able to reference the same Gitea server. +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. + + Deprecates: + + - `getGitHubFileFetchUrl` replaced by `getGithubFileFetchUrl` + - `GitHubIntegrationConfig` replaced by `GithubIntegrationConfig` + - `GitHubIntegration` replaced by `GithubIntegration` + - `readGitHubIntegrationConfig` replaced by `readGithubIntegrationConfig` + - `readGitHubIntegrationConfigs` replaced by `readGithubIntegrationConfigs` + - `replaceGitHubUrlType` replaced by `replaceGithubUrlType` + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies + +- a6d779d58a: Remove explicit default visibility at `config.d.ts` files. + + ```ts + /** + * @visibility backend + */ + ``` + +- Updated dependencies + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-azure-sites@0.1.0 + +### Minor Changes + +- 4a75ce761c: Azure Sites (Apps & Functions) support for a given entity. View the current status of the site, quickly jump to site's Overview page, or Log Stream page. + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-azure-sites-backend@0.1.0 + +### Minor Changes + +- 4a75ce761c: Azure Sites (Apps & Functions) support for a given entity. View the current status of the site, quickly jump to site's Overview page, or Log Stream page. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/config@1.0.4 + +## @backstage/plugin-azure-sites-common@0.1.0 + +### Minor Changes + +- 4a75ce761c: Azure Sites (Apps & Functions) support for a given entity. View the current status of the site, quickly jump to site's Overview page, or Log Stream page. + +## @backstage/plugin-bazaar@0.2.0 + +### Minor Changes + +- 28b39e0e0e: The limit prop of BazaarOverviewCard has been removed entirely, and instead replaced with a new optional boolean prop `fullWidth`. The BazaarOverviewCard now always use full height without fixed width. Also fixed problem with link to Bazaar. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/cli@0.21.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-catalog@1.6.1 + - @backstage/catalog-client@1.1.2 + - @backstage/errors@1.1.3 + +## @backstage/plugin-catalog-backend-module-github@0.2.0 + +### Minor Changes + +- 67fe5bc9a9: BREAKING: Support authenticated backends by including a server token for catalog requests. The constructor of `GithubLocationAnalyzer` now requires an instance of `TokenManager` to be supplied: + + ```diff + ... + builder.addLocationAnalyzers( + new GitHubLocationAnalyzer({ + discovery: env.discovery, + config: env.config, + + tokenManager: env.tokenManager, + }), + ); + ... + ``` + +- f64d66a45c: Added the ability for the GitHub discovery provider to validate that catalog files exist before emitting them. + + Users can now set the `validateLocationsExist` property to `true` in their GitHub discovery configuration to opt in to this feature. + This feature only works with `catalogPath`s that do not contain wildcards. + + When `validateLocationsExist` is set to `true`, the GitHub discovery provider will retrieve the object from the + repository at the provided `catalogPath`. + If this file exists and is non-empty, then it will be emitted as a location for further processing. + If this file does not exist or is empty, then it will not be emitted. + Not emitting locations that do not exist allows for far fewer calls to the GitHub API to validate locations that do not exist. + +### Patch Changes + +- 67fe5bc9a9: Properly derive Github credentials when making requests in `GithubLocationAnalyzer` to support Github App authentication +- bef063dc8d: - Make it possible to inject custom user and team transformers when configuring the `GithubOrgEntityProvider` +- 4c9f7847e4: Updated dependency `msw` to `^0.48.0` while moving it to be a dev dependency. +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-cost-insights@0.12.0 + +### Minor Changes + +- 43afded227: Updated recharts to v2.0.0 and fixed typing issues + +### Patch Changes + +- e92aa15f01: Bumped `canvas` dependency to the latest version, which has better Node.js v18 support. +- cbe11d1e23: Tweak README +- 745e0e2228: Added support for displaying entity cost insights by implementing the new `getCatalogEntityDailyCost` that's part of the `CostInsightsApi`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-events-backend@0.1.0 + +### Minor Changes + +- dc9da28abd: Support events received via HTTP endpoints at plugin-events-backend. + + The plugin provides an event publisher `HttpPostIngressEventPublisher` + which will allow you to receive events via + HTTP endpoints `POST /api/events/http/{topic}` + and will publish these to the used event broker. + + Using a provided custom validator, you can participate in the decision + which events are accepted, e.g. by verifying the source of the request. + + Please find more information at + . + +- 7bbd2403a1: Adds a new backend plugin plugin-events-backend for managing events. + + plugin-events-node exposes interfaces which can be used by modules. + + plugin-events-backend-test-utils provides utilities which can be used while writing tests e.g. for modules. + + Please find more information at + . + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/config@1.0.4 + +## @backstage/plugin-events-backend-module-aws-sqs@0.1.0 + +### Minor Changes + +- d3ecb2382d: Adds a new module `aws-sqs` for plugin-events-backend. + + The module provides an event publisher `AwsSqsConsumingEventPublisher` + which will allow you to receive events from + an AWS SQS queue and will publish these to the used event broker. + + Please find more information at + . + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.3.7 + - @backstage/plugin-events-node@0.1.0 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/config@1.0.4 + +## @backstage/plugin-events-backend-module-azure@0.1.0 + +### Minor Changes + +- 12cd94b7e9: Adds a new module `azure` to plugin-events-backend. + + The module adds a new event router `AzureDevOpsEventRouter`. + + The event router will re-publish events received at topic `azureDevOps` + under a more specific topic depending on their `$.eventType` value + (e.g., `azureDevOps.git.push`). + + Please find more information at + . + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.0 + +### Minor Changes + +- 6bc121bf0d: Adds a new module `bitbucket-cloud` to plugin-events-backend. + + The module adds a new event router `BitbucketCloudEventRouter`. + + The event router will re-publish events received at topic `bitbucketCloud` + under a more specific topic depending on their `x-event-key` value + (e.g., `bitbucketCloud.repo:push`). + + Please find more information at + . + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 + +## @backstage/plugin-events-backend-module-gerrit@0.1.0 + +### Minor Changes + +- 25f6d7bddb: Adds a new module `gerrit` to plugin-events-backend. + + The module adds a new event router `GerritEventRouter`. + + The event router will re-publish events received at topic `gerrit` + under a more specific topic depending on their `$.type` value + (e.g., `gerrit.change-merged`). + + Please find more information at + . + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 + +## @backstage/plugin-events-backend-module-github@0.1.0 + +### Minor Changes + +- b3a4edb885: Adds a new module `github` to plugin-events-backend. + + The module adds a new event router `GithubEventRouter`. + + The event router will re-publish events received at topic `github` + under a more specific topic depending on their `x-github-event` value + (e.g., `github.push`). + + Please find more information at + . + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 + +## @backstage/plugin-events-backend-module-gitlab@0.1.0 + +### Minor Changes + +- 63f7983398: Adds a new module `gitlab` to plugin-events-backend. + + The module adds a new event router `GitlabEventRouter`. + + The event router will re-publish events received at topic `gitlab` + under a more specific topic depending on their `$.event_name` value + (e.g., `gitlab.push`). + + Please find more information at + . + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 + +## @backstage/plugin-events-backend-test-utils@0.1.0 + +### Minor Changes + +- 7bbd2403a1: Adds a new backend plugin plugin-events-backend for managing events. + + plugin-events-node exposes interfaces which can be used by modules. + + plugin-events-backend-test-utils provides utilities which can be used while writing tests e.g. for modules. + + Please find more information at + . + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + +## @backstage/plugin-events-node@0.1.0 + +### Minor Changes + +- dc9da28abd: Support events received via HTTP endpoints at plugin-events-backend. + + The plugin provides an event publisher `HttpPostIngressEventPublisher` + which will allow you to receive events via + HTTP endpoints `POST /api/events/http/{topic}` + and will publish these to the used event broker. + + Using a provided custom validator, you can participate in the decision + which events are accepted, e.g. by verifying the source of the request. + + Please find more information at + . + +- 7bbd2403a1: Adds a new backend plugin plugin-events-backend for managing events. + + plugin-events-node exposes interfaces which can be used by modules. + + plugin-events-backend-test-utils provides utilities which can be used while writing tests e.g. for modules. + + Please find more information at + . + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.4 + +## @backstage/plugin-github-issues@0.2.0 + +### Minor Changes + +- ead285b9e4: **BREAKING**: Changed the casing of all exported types to have a lowercase "h" in "github". E.g. "GitHubIssuesPage" was renamed to "GithubIssuesPage". Please rename your imports where necessary. + +### Patch Changes + +- c8dd2a8c87: Stripping specific issues URL already present to target base issues URL. +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-ilert@0.2.0 + +### Minor Changes + +- 0697af30da: Added support for multiple responders in alert list, added new tab with list to support iLert resource 'service', added new tab with list to support iLert resource 'status page' + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-kubernetes-backend@0.8.0 + +### Minor Changes + +- cbf5d11fdf: The Kubernetes errors when fetching pod metrics are now captured and returned to the frontend. + + - **BREAKING** The method `fetchPodMetricsByNamespace` in the interface `KubernetesFetcher` is changed to `fetchPodMetricsByNamespaces`. It now accepts a set of namespace strings and returns `Promise`. + - Add the `PodStatusFetchResponse` to the `FetchResponse` union type. + - Add `NOT_FOUND` to the `KubernetesErrorTypes` union type, the HTTP error with status code 404 will be mapped to this error. + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-kubernetes-common@0.4.4 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-org@0.6.0 + +### Minor Changes + +- 0b11500151: Updates the User and Group Profile cards to add the links from the UserEntity or the GroupEntity + +### Patch Changes + +- 38dd29ea95: Add entity kind to the Ownership Cards. Fix the query parameters for the links of the Ownership Cards so that the catalog page actually selects the right entity kind. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-org-react@0.1.0 + +### Minor Changes + +- e96274f1fe: Implemented the org-react plugin, with it's first component being: a `GroupListPicker` component that will give the user the ability to choose a group + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/theme@0.2.16 + +## @backstage/plugin-scaffolder@1.8.0 + +### Minor Changes + +- edae17309e: Added props to override default Scaffolder page title, subtitle and pageTitleOverride. + Routes like `rootRouteRef`, `selectedTemplateRouteRef`, `nextRouteRef`, `nextSelectedTemplateRouteRef` were made public and can be used in your app (e.g. in custom TemplateCard component). + +### Patch Changes + +- 580285787d: The `create` and `click` analytics events are now also captured on the "next" version of the component creation page. + +- 4830a3569f: Basic analytics instrumentation is now in place: + + - As users make their way through template steps, a `click` event is fired, including the step number. + - After a user clicks "Create" a `create` event is fired, including the name of the software that was just created. The template used at creation is set on the `entityRef` context key. + +- 94b7ca9c6d: Updated to use `@rjsf` packages of version `^5.0.0-beta.12` + +- 87840c8c6c: Fixed tiny grammar error in EntityNamePicker. The first letter of the description is now capitalized. + +- 3b3fc3cc3c: Fix `formData` not being present in the `next` version + +- b2bb48a6f4: Fix the return type for the `createNextScaffodlerFieldExtension` type as before it wasn't a component type for the extension + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/integration-react@1.1.6 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-permission-react@0.4.7 + - @backstage/plugin-scaffolder-common@1.2.2 + +## @backstage/plugin-scaffolder-backend@1.8.0 + +### Minor Changes + +- ea14eb62a2: Added a set of default Prometheus metrics around scaffolding. See below for a list of metrics and an explanation of their labels: + + - `scaffolder_task_count`: Tracks successful task runs. + + Labels: + + - `template`: The entity ref of the scaffolded template + - `user`: The entity ref of the user that invoked the template run + - `result`: A string describing whether the task ran successfully, failed, or was skipped + + - `scaffolder_task_duration`: a histogram which tracks the duration of a task run + + Labels: + + - `template`: The entity ref of the scaffolded template + - `result`: A boolean describing whether the task ran successfully + + - `scaffolder_step_count`: a count that tracks each step run + + Labels: + + - `template`: The entity ref of the scaffolded template + - `step`: The name of the step that was run + - `result`: A string describing whether the task ran successfully, failed, or was skipped + + - `scaffolder_step_duration`: a histogram which tracks the duration of each step run + + Labels: + + - `template`: The entity ref of the scaffolded template + - `step`: The name of the step that was run + - `result`: A string describing whether the task ran successfully, failed, or was skipped + + You can find a guide for running Prometheus metrics here: + +- 5921b5ce49: - The GitLab Project ID for the `publish:gitlab:merge-request` action is now passed through the query parameter `project` in the `repoUrl`. It still allows people to not use the `projectid` and use the `repoUrl` with the `owner` and `repo` query parameters instead. This makes it easier to publish to repositories instead of writing the full path to the project. + +- 5025d2e8b6: Adds the ability to pass (an optional) array of strings that will be applied to the newly scaffolded repository as topic labels. + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- 969a8444ea: Updated dependency `esbuild` to `^0.15.0`. +- 9ff4ff3745: Implement "Branch protection rules" support for "publish:github" action +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-scaffolder-common@1.2.2 + +## @backstage/plugin-sonarqube@0.5.0 + +### Minor Changes + +- 786117e98a: Fix sonarqube annotation parsing. Add content page for Sonarqube. + Removed the deprecated `plugin` export; please use `sonarQubePlugin` instead. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-splunk-on-call@0.4.0 + +### Minor Changes + +- 34b772ef31: Use the routing key if it's available instead of team name when triggering incidents. + + BREAKING CHANGE: + Before, the team name was used even if the routing key (with or without team) was used. + Now, the routing key defined for the component will be used instead of the team name. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs@1.4.0 + +### Minor Changes + +- 5691baea69: Add ability to configure filters when using EntityListDocsGrid + + The following example will render two sections of cards grid: + + - One section for documentations tagged as `recommended` + - One section for documentations tagged as `runbook` + + ```js + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + { + title: "RunBooks Documentation", + filterPredicate: entity => + entity?.metadata?.tags?.includes('runbook') ?? false, + } + ]}} /> + ``` + +- 63705e73d9: Hide document description if not provided + +- 847fc588a6: Updated TechDocs header to include label for source code icon and updated label to reflect Kind name + +### Patch Changes + +- 9e4d8e6198: Fix logic bug that broke techdocs-cli-embedded-app +- e92aa15f01: Bumped `canvas` dependency to the latest version, which has better Node.js v18 support. +- cbe11d1e23: Tweak README +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- 3a1a999b7b: Include query parameters when navigating to relative links in documents +- bd2aab4726: An analytics event matching the semantics of the `click` action is now captured when users click links within a TechDocs document. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-search-react@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/app-defaults@1.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-app-api@1.2.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.7 + +## @backstage/backend-app-api@0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-permission-node@0.7.1 + - @backstage/errors@1.1.3 + +## @backstage/backend-defaults@0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.2.3 + - @backstage/backend-plugin-api@0.1.4 + +## @backstage/backend-plugin-api@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/config@1.0.4 + +## @backstage/backend-tasks@0.3.7 + +### Patch Changes + +- 30e43717c7: Deprecated the `HumanDuration` type, which should now instead be imported from `@backstage/types`. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/backend-test-utils@0.1.30 + +### Patch Changes + +- e13cd3feaf: Updated dependency `msw` to `^0.48.0`. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/cli@0.21.0 + - @backstage/backend-app-api@0.2.3 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/config@1.0.4 + +## @backstage/catalog-client@1.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + +## @backstage/catalog-model@1.1.3 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- Updated dependencies + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/codemods@0.1.41 + +### Patch Changes + +- 58502ec285: Updated dependency `jscodeshift` to `^0.14.0`. +- Updated dependencies + - @backstage/cli-common@0.1.10 + +## @backstage/config@1.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1 + +## @backstage/config-loader@1.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/create-app@0.4.34 + +### Patch Changes + +- f1c3cdfb2d: Bumped create-app version. + +- df21bbd4ad: The [Analytics API](https://backstage.io/docs/plugins/analytics) is the recommended way to track usage in Backstage; an optionally installable [Google Analytics module](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-ga#installation) has superseded the old app.googleAnalyticsTrackingId config and its corresponding script tags in packages/app/public/index.html. + + For an existing installation where you want to remove the redundant app.googleAnalyticsTrackingId, you should make the following adjustment to `packages/app/public/index.html`: + + ```diff + <%= config.getString('app.title') %> + - <% if (config.has('app.googleAnalyticsTrackingId')) { %> + - + - + - <% } %> + + ``` + + Additionally, you should make the following adjustment to `app-config.yaml`: + + ```diff + app: + title: Backstage Example App + baseUrl: http://localhost:3000 + - googleAnalyticsTrackingId: # UA-000000-0 + ``` + +- 4091c73e68: Updated `@swc/core` to `v1.3.9` which fixes a `.tsx` parser bug. You may want to run `yarn backstage-cli versions:bump` to get on latest version including the CLI itself. + +- 80bfac5266: Updated the create-app command to no longer require Git to be installed and configured. A git repository will only be initialized if possible and if not already in an git repository. + +- 286d474675: The `build` script in the root `package.json` has been split into two separate scripts, `build:backend` and `build:all`. This is to more accurately reflect what is being built, to avoid confusion. + + If you want to build the project for a production deployment, you will want to use `build:backend`, as this builds both the frontend and backend package. If you are not using the `app-backend` plugin you will want to add your own `build:frontend` script, to which you can pass additional configuration parameters if needed. + + The `build:all` script is useful if you simply want to check that it is possible to build all packages in the project. This might be useful as a CI check, but is generally unnecessary. + + If you want to publish the packages in your repository you can add a `build:packages` script that calls `backstage-cli repo build`. This will skip the frontend and backend packages builds, as those are quite time consuming. + + To apply these changes to an existing project, make the following change to the root `package.json` file: + + ```diff + - "build": "backstage-cli repo build --all", + + "build:backend": "yarn workspace backend build", + + "build:all": "backstage-cli repo build --all", + ``` + + There are also a couple of places where documentation has been updated, see the [upgrade helper](https://backstage.github.io/upgrade-helper/?to=1.8.0) for a full list of changes. + +- 384eaa2307: Switched Node.js version to support version 16 & 18, rather than 14 & 16. To switch the Node.js version in your own project, apply the following change to the root `package.json`: + + ```diff + "engines": { + - "node": "14 || 16" + + "node": "16 || 18" + }, + ``` + + As well as the following change to `packages/app/package.json`: + + ```diff + - "@types/node": "^14.14.32", + + "@types/node": "^16.11.26", + ``` + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. + +- 864c876e57: Fixed incorrect comments in the templated `app-config.yaml` and `app-config.production.yaml`. The `backend.listen` directive is not in fact needed to override the `backend.baseUrl`, the backend listens to all interfaces by default. The configuration has also been updated to listen to all interfaces, rather than just IPv4 ones, as this is required for Node.js v18. The production configuration now also shows the option to specify `backend.listen` as a single string. + + To apply this changes to an existing app, make the following change to `app-config.yaml`: + + ```diff + - # Uncomment the following host directive to bind to all IPv4 interfaces and + - # not just the baseUrl hostname. + - # host: 0.0.0.0 + + # Uncomment the following host directive to bind to specific interfaces + + # host: 127.0.0.1 + ``` + + And the following change to `app-config.production.yaml`: + + ```diff + - listen: + - port: 7007 + - # The following host directive binds to all IPv4 interfaces when its value + - # is "0.0.0.0". This is the most permissive setting. The right value depends + - # on your specific deployment. If you remove the host line entirely, the + - # backend will bind on the interface that corresponds to the backend.baseUrl + - # hostname. + - host: 0.0.0.0 + + # The listener can also be expressed as a single : string. In this case we bind to + + # all interfaces, the most permissive setting. The right value depends on your specific deployment. + + listen: ':7007' + ``` + +- Updated dependencies + - @backstage/cli-common@0.1.10 + +## @backstage/dev-utils@1.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-app-api@1.2.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/app-defaults@1.0.8 + - @backstage/integration-react@1.1.6 + - @backstage/test-utils@1.2.2 + - @backstage/theme@0.2.16 + +## @backstage/errors@1.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1 + +## @backstage/integration-react@1.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## @backstage/release-manifests@0.0.7 + +### Patch Changes + +- a4496131fa: Added a fallback that fetches manifests from `https://raw.githubusercontent.com` if `https://versions.backstage.io` is unavailable. + +## @techdocs/cli@1.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-techdocs-node@1.4.2 + - @backstage/catalog-model@1.1.3 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4 + +## @backstage/test-utils@1.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.2.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.7 + +## @backstage/types@1.0.1 + +### Patch Changes + +- 30e43717c7: Added the `HumanDuration` type, moved here from `@backstage/backend-tasks`. This type matches the `Duration.fromObject` form of `luxon`. + +## @backstage/version-bridge@1.0.2 + +### Patch Changes + +- e70984325d: Updated the value created by `createVersionedValueMap` to give it an extra hidden property that makes it easier to inspect during debugging. + +## @backstage/plugin-adr@0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-search-react@1.2.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.3 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-adr-backend@0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-adr-common@0.2.3 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-adr-common@0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-airbrake@0.3.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/dev-utils@1.0.8 + - @backstage/test-utils@1.2.2 + - @backstage/theme@0.2.16 + +## @backstage/plugin-airbrake-backend@0.2.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + +## @backstage/plugin-allure@0.1.27 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-analytics-module-ga@0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apache-airflow@0.2.4 + +### Patch Changes + +- 989731cabf: 1. Added a new column in the table to quickly view the latest DAG runs, plus a link to it if you want to have a deeper look. 2. Table columns are togglable 3. Set hidden columns 4. Fixed bug with turning on/off the DAGs +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + +## @backstage/plugin-api-docs@0.8.11 + +### Patch Changes + +- 4db78c2296: Updated dependency `@asyncapi/react-component` to `1.0.0-next.44`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-catalog@1.6.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apollo-explorer@0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-app-backend@0.3.38 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/config@1.0.4 + - @backstage/config-loader@1.1.6 + +## @backstage/plugin-auth-backend@0.17.1 + +### Patch Changes + +- 0d6837ca4e: Fix wrong GitHub callback URL documentation +- cbe11d1e23: Tweak README +- 89d705e806: Add support for custom JWT header name in GCP IAP auth. +- abaed9770e: Improve logging +- d80833fe0c: Inject optional `CatalogApi` into auth-backend `createRouter` function. This will enable developers to use customized `CatalogApi` when creating the router. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/types@1.0.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-auth-node@0.2.7 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-azure-devops@0.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-devops-backend@0.3.17 + +### Patch Changes + +- 62f284e394: - Adjusted the asset parser to accept case sensitive + - Fixed fetching data that was using the deprecated function +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-badges@0.2.35 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-badges-backend@0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-bazaar-backend@0.2.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/backend-test-utils@0.1.30 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-bitbucket-cloud-common@0.2.1 + +### Patch Changes + +- d089fbe7dc: Add interfaces for Bitbucket Cloud (webhook) events. +- Updated dependencies + - @backstage/integration@1.4.0 + +## @backstage/plugin-bitrise@0.1.38 + +### Patch Changes + +- 43afded227: Updated recharts to v2.0.0 and fixed typing issues +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-catalog@1.6.1 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-search-react@1.2.1 + - @backstage/catalog-client@1.1.2 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-catalog-backend@1.5.1 + +### Patch Changes + +- c1a4addda3: Improve processing error logging. + + Adds `location` and `owner` to the logging meta if they are available. + +- a7607b5413: Replace usage of deprecataed `UrlReader.read` with `UrlReader.readUrl`. + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/plugin-permission-node@0.7.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-scaffolder-common@1.2.2 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-catalog-backend-module-aws@0.1.11 + +### Patch Changes + +- bae3617be5: `AwsS3EntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + + +- defb389ecd: Add `awsS3EntityProviderCatalogModule` (new backend-plugin-api, alpha). + +- a6d779d58a: Remove explicit default visibility at `config.d.ts` files. + + ```ts + /** + * @visibility backend + */ + ``` + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-catalog-backend-module-azure@0.1.9 + +### Patch Changes + +- 87ff05892d: `AzureDevOpsEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + + +- 0ca399b31b: Add `azureDevOpsEntityProviderCatalogModule` (new backend-plugin-api, alpha). + +- 4c9f7847e4: Updated dependency `msw` to `^0.48.0` while moving it to be a dev dependency. + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.5 + +### Patch Changes + +- 4c9f7847e4: Updated dependency `msw` to `^0.48.0` while moving it to be a dev dependency. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.5 + +### Patch Changes + +- d089fbe7dc: Handle Bitbucket Cloud `repo:push` events at the `BitbucketCloudEntityProvider` + by subscribing to the topic `bitbucketCloud.repo:push.` + + Implements `EventSubscriber` to receive events for the topic `bitbucketCloud.repo:push`. + + On `repo:push`, the affected repository will be refreshed. + This includes adding new Location entities, refreshing existing ones, + and removing obsolete ones. + + To support this, a new annotation `bitbucket.org/repo-url` was added + to Location entities. + + A full refresh will require 1 API call to Bitbucket Cloud to discover all catalog files. + When we handle one `repo:push` event, we also need 1 API call in order to know + which catalog files exist. + This may lead to more discovery-related API calls (code search). + The main cause for hitting the rate limits are Locations refresh-related operations. + + A reduction of total API calls to reduce the rate limit issues can only be achieved in + combination with + + 1. reducing the full refresh frequency (e.g., to monthly) + 2. reducing the frequency of general Location refresh operations by the processing loop + + For (2.), it is not possible to reduce the frequency only for Bitbucket Cloud-related + Locations though. + + Further optimizations might be required to resolve the rate limit issue. + + **Installation and Migration** + + Please find more information at + , + in particular the section about "_Installation with Events Support_". + + In case of the new backend-plugin-api _(alpha)_ the module will take care of + registering itself at both. + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-events-node@0.1.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/plugin-catalog-common@1.0.8 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.3 + +### Patch Changes + +- 68f7f5a857: `BitbucketServerEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + + +- cd48ed8370: Add `bitbucketServerEntityProviderCatalogModule` (new backend-plugin-api, alpha). + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.6 + +### Patch Changes + +- 4fba50f5d4: Add `gerritEntityProviderCatalogModule` (new backend-plugin-api, alpha). + +- 4c9f7847e4: Updated dependency `msw` to `^0.48.0` while moving it to be a dev dependency. + +- 134b69f478: `GerritEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + + +- a6d779d58a: Remove explicit default visibility at `config.d.ts` files. + + ```ts + /** + * @visibility backend + */ + ``` + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.9 + +### Patch Changes + +- 6bb046bcbe: Add `gitlabDiscoveryEntityProviderCatalogModule` (new backend-plugin-api, alpha). + +- 81cedb5033: `GitlabDiscoveryEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + + +- 4c9f7847e4: Updated dependency `msw` to `^0.48.0` while moving it to be a dev dependency. + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-catalog-backend-module-msgraph@0.4.4 + +### Patch Changes + +- 0e37858f22: Added cause information to logged warnings + +- 8d1a5e08ca: `MicrosoftGraphOrgEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + + +- 384f99c276: Add `microsoftGraphOrgEntityProviderCatalogModule` (new backend-plugin-api, alpha). + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.4 + +### Patch Changes + +- 4ce887400d: Added support to use the `UrlReaders` when `$ref` pointing to a URL. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + +## @backstage/plugin-catalog-common@1.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-catalog-graph@0.2.23 + +### Patch Changes + +- da0bf25d1a: Preserve graph options and increment `maxDepth` by 1. + + The change will preserve options used at the `CatalogGraphCard` + (displayed at the entity page) and additionally, increments the + `maxDepth` option by 1 to increase the scope slightly compared to + the graph already seen by the users. + + The default for `maxDepth` at `CatalogGraphCard` is 1. + +- 21d84ef332: feat: Enable theme overrides for components in catalog-graph plugin + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/theme@0.2.16 + +## @backstage/plugin-catalog-graphql@0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + +## @backstage/plugin-catalog-import@0.9.1 + +### Patch Changes + +- 1e7b640518: Get rid of `this-is-undefined-in-esm` warning +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/integration-react@1.1.6 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-catalog-common@1.0.8 + +## @backstage/plugin-catalog-node@1.2.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/catalog-client@1.1.2 + - @backstage/errors@1.1.3 + - @backstage/plugin-catalog-common@1.0.8 + +## @backstage/plugin-catalog-react@1.2.1 + +### Patch Changes + +- a889314692: Both `EntityProvider` and `AsyncEntityProvider` contexts now wrap all children with an `AnalyticsContext` containing the corresponding `entityRef`; this opens up the possibility for all events underneath these contexts to be associated with and aggregated by the corresponding entity. +- e47f466f80: Removed forced capitalization for Entity types in the catalog sidebar. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/version-bridge@1.0.2 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/types@1.0.1 + - @backstage/catalog-client@1.1.2 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-permission-react@0.4.7 + +## @backstage/plugin-cicd-statistics@0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-cicd-statistics@0.1.13 + +## @backstage/plugin-circleci@0.3.11 + +### Patch Changes + +- 383574c49b: Update screenshots in documentation to match latest CircleCI plugin +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-cloudbuild@0.3.11 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-climate@0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-coverage@0.2.4 + +### Patch Changes + +- fcab2579a0: Adds installation instructions +- 43afded227: Updated recharts to v2.0.0 and fixed typing issues +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-coverage-backend@0.2.4 + +### Patch Changes + +- fcab2579a0: Adds installation instructions +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-codescene@0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-config-schema@0.1.34 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-dynatrace@1.0.1 + +### Patch Changes + +- cbe11d1e23: Tweak README +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-explore@0.3.42 + +### Patch Changes + +- 5c25ce6d9e: Added a section to explore plugin README that describes the customization of explore tools content. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-explore-react@0.0.23 + +## @backstage/plugin-explore-react@0.0.23 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.0 + +## @backstage/plugin-firehydrant@0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-fossa@0.2.43 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcalendar@0.3.7 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcp-projects@0.3.30 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-git-release-manager@0.3.24 + +### Patch Changes + +- 43afded227: Updated recharts to v2.0.0 and fixed typing issues +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-actions@0.5.11 + +### Patch Changes + +- ed438a3ba5: Add error panel when the plugin fails. +- 0d6837ca4e: Fix wrong GitHub callback URL documentation +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-deployments@0.1.42 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/integration-react@1.1.6 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-pull-requests-board@0.1.5 + +### Patch Changes + +- cc8bfc56c3: Add a new "Team" Filter Options to the Github Pull Requests Dashboard. + + When toggling this option on, the dashboard will displays all of the PRs opened + by the members of that team on any repositories of the organization. + +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gitops-profiles@0.3.29 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gocd@0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphiql@0.2.43 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphql-backend@0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + - @backstage/plugin-catalog-graphql@0.3.15 + +## @backstage/plugin-home@0.4.27 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-stack-overflow@0.1.7 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-jenkins@0.7.10 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-jenkins-common@0.1.10 + +## @backstage/plugin-jenkins-backend@0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-jenkins-common@0.1.10 + +## @backstage/plugin-jenkins-common@0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.1 + - @backstage/plugin-catalog-common@1.0.8 + +## @backstage/plugin-kafka@0.3.11 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-kafka-backend@0.2.31 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-kubernetes@0.7.4 + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-kubernetes-common@0.4.4 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-kubernetes-common@0.4.4 + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. + +- cbf5d11fdf: The Kubernetes errors when fetching pod metrics are now captured and returned to the frontend. + + - **BREAKING** The method `fetchPodMetricsByNamespace` in the interface `KubernetesFetcher` is changed to `fetchPodMetricsByNamespaces`. It now accepts a set of namespace strings and returns `Promise`. + - Add the `PodStatusFetchResponse` to the `FetchResponse` union type. + - Add `NOT_FOUND` to the `KubernetesErrorTypes` union type, the HTTP error with status code 404 will be mapped to this error. + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + +## @backstage/plugin-lighthouse@0.3.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic@0.3.29 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic-dashboard@0.2.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + +## @backstage/plugin-pagerduty@0.5.4 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-periskop@0.1.9 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-periskop-backend@0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + +## @backstage/plugin-permission-backend@0.5.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/plugin-permission-node@0.7.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-permission-common@0.7.1 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- 64848c963c: Properly handle rules that have no parameters in `PermissionClient` +- Updated dependencies + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-permission-node@0.7.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-permission-react@0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/config@1.0.4 + +## @backstage/plugin-playlist@0.1.2 + +### Patch Changes + +- 605f269f0d: Updated Playlist plugin docs: + + - Updated `playlist` plugin README to include note about installing backend plugin and added images for the various features + - Updated `playlist-backend` plugin README to remove `IdentityClient` import in example as it is not used and made minor change to headings + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/plugin-search-react@1.2.1 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-permission-react@0.4.7 + - @backstage/plugin-playlist-common@0.1.2 + +## @backstage/plugin-playlist-backend@0.2.1 + +### Patch Changes + +- 605f269f0d: Updated Playlist plugin docs: + + - Updated `playlist` plugin README to include note about installing backend plugin and added images for the various features + - Updated `playlist-backend` plugin README to remove `IdentityClient` import in example as it is not used and made minor change to headings + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/backend-test-utils@0.1.30 + - @backstage/plugin-permission-node@0.7.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-playlist-common@0.1.2 + +## @backstage/plugin-playlist-common@0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.1 + +## @backstage/plugin-proxy-backend@0.2.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + +## @backstage/plugin-rollbar@0.4.11 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-rollbar-backend@0.1.35 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-scaffolder-backend@1.8.0 + - @backstage/integration@1.4.0 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-scaffolder-backend@1.8.0 + - @backstage/integration@1.4.0 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + +## @backstage/plugin-scaffolder-common@1.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + +## @backstage/plugin-search@1.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/version-bridge@1.0.2 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/plugin-search-react@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-search-backend@1.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/types@1.0.1 + - @backstage/plugin-permission-node@0.7.1 + - @backstage/plugin-search-backend-node@1.0.4 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.4 + - @backstage/config@1.0.4 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-search-backend-module-pg@0.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-search-backend-node@1.0.4 + - @backstage/config@1.0.4 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-search-backend-node@1.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-search-common@1.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.1 + - @backstage/types@1.0.1 + +## @backstage/plugin-search-react@1.2.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/version-bridge@1.0.2 + - @backstage/core-plugin-api@1.1.0 + - @backstage/types@1.0.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-sentry@0.4.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-shortcuts@0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/types@1.0.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-sonarqube-backend@0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-stack-overflow@0.1.7 + +### Patch Changes + +- e32d5643e3: `StackOverflowSearchResultListItem` now accept optional rank property to be able to capture rank analytics data. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-home@0.4.27 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-stack-overflow-backend@0.1.7 + +### Patch Changes + +- cbe11d1e23: Tweak README + +- a6d779d58a: Remove explicit default visibility at `config.d.ts` files. + + ```ts + /** + * @visibility backend + */ + ``` + +- Updated dependencies + - @backstage/cli@0.21.0 + - @backstage/config@1.0.4 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-tech-insights@0.3.3 + +### Patch Changes + +- 7095e8bc03: Fixed bug when sending data by Post in `runChecks` and `runBulkChecks` functions of the `TechInsightsClient` class, the default `Content-Type` used was `plain/text` +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-tech-insights-common@0.2.8 + +## @backstage/plugin-tech-insights-backend@0.5.4 + +### Patch Changes + +- f12e9e5b8c: Add Documentation on 404 Errors +- 06cf8f1cf2: Add a default delay to the fact retrievers to prevent cold-start errors +- 30e43717c7: Use `HumanDuration` from `@backstage/types` +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/plugin-tech-insights-node@0.3.6 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-tech-insights-common@0.2.8 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-tech-insights-node@0.3.6 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-tech-insights-common@0.2.8 + +## @backstage/plugin-tech-insights-common@0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1 + +## @backstage/plugin-tech-insights-node@0.3.6 + +### Patch Changes + +- 06cf8f1cf2: Add a default delay to the fact retrievers to prevent cold-start errors +- 30e43717c7: Use `HumanDuration` from `@backstage/types` +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/plugin-tech-insights-common@0.2.8 + +## @backstage/plugin-tech-radar@0.5.18 + +### Patch Changes + +- 1f888af5f6: Fixed bug in Tech Radar where, on hover, the tech list quadrant would rerender and scroll top +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0 + - @backstage/core-components@0.12.0 + - @backstage/core-app-api@1.2.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/plugin-catalog@1.6.1 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-search-react@1.2.1 + - @backstage/test-utils@1.2.2 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs-backend@1.4.1 + +### Patch Changes + +- a7607b5413: Replace usage of deprecataed `UrlReader.read` with `UrlReader.readUrl`. + +- a6d779d58a: Remove explicit default visibility at `config.d.ts` files. + + ```ts + /** + * @visibility backend + */ + ``` + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-techdocs-node@1.4.2 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.6 + +### Patch Changes + +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/integration-react@1.1.6 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs-node@1.4.2 + +### Patch Changes + +- a7607b5413: Replace usage of deprecataed `UrlReader.read` with `UrlReader.readUrl`. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-search-common@1.1.1 + +## @backstage/plugin-techdocs-react@1.0.6 + +### Patch Changes + +- 7d1352d459: Analytics events captured within the `` now include the conventional `entityRef` context value, associating those events with their corresponding entity. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/version-bridge@1.0.2 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + +## @backstage/plugin-todo@0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-todo-backend@0.1.35 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-user-settings@0.5.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-app-api@1.2.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/types@1.0.1 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-user-settings-backend@0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/types@1.0.1 + - @backstage/errors@1.1.3 + +## @backstage/plugin-vault@0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## @backstage/plugin-vault-backend@0.2.4 + +### Patch Changes + +- 687237da4c: Added `errorHandler()` middleware to `router` to prevent crashes caused by fatal errors in plugin backend +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/backend-test-utils@0.1.30 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @backstage/plugin-xcmetrics@0.2.31 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- 43afded227: Updated recharts to v2.0.0 and fixed typing issues +- dcf9e728de: Removed an unused and hidden build details route. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## example-app@0.2.77 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.8.0 + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/plugin-techdocs@1.4.0 + - @backstage/core-components@0.12.0 + - @backstage/plugin-cost-insights@0.12.0 + - @backstage/plugin-apache-airflow@0.2.4 + - @backstage/core-app-api@1.2.0 + - @backstage/plugin-explore@0.3.42 + - @backstage/plugin-tech-insights@0.3.3 + - @backstage/plugin-github-actions@0.5.11 + - @backstage/cli@0.21.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-dynatrace@1.0.1 + - @backstage/plugin-catalog-import@0.9.1 + - @backstage/plugin-catalog-graph@0.2.23 + - @backstage/plugin-tech-radar@0.5.18 + - @backstage/plugin-playlist@0.1.2 + - @backstage/plugin-stack-overflow@0.1.7 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-gcalendar@0.3.7 + - @backstage/plugin-code-coverage@0.2.4 + - @backstage/plugin-org@0.6.0 + - @backstage/plugin-api-docs@0.8.11 + - @backstage/plugin-kubernetes@0.7.4 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.6 + - @backstage/plugin-circleci@0.3.11 + - @backstage/plugin-azure-sites@0.1.0 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/plugin-cloudbuild@0.3.11 + - @backstage/plugin-jenkins@0.7.10 + - @backstage/plugin-kafka@0.3.11 + - @backstage/plugin-pagerduty@0.5.4 + - @backstage/plugin-rollbar@0.4.11 + - @backstage/plugin-airbrake@0.3.11 + - @backstage/plugin-azure-devops@0.2.2 + - @backstage/plugin-badges@0.2.35 + - @internal/plugin-catalog-customized@0.0.4 + - @backstage/plugin-gocd@0.1.17 + - @backstage/plugin-home@0.4.27 + - @backstage/plugin-lighthouse@0.3.11 + - @backstage/plugin-newrelic-dashboard@0.2.4 + - @backstage/plugin-search@1.0.4 + - @backstage/plugin-sentry@0.4.4 + - @backstage/plugin-todo@0.2.13 + - @backstage/app-defaults@1.0.8 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-gcp-projects@0.3.30 + - @backstage/plugin-graphiql@0.2.43 + - @backstage/plugin-newrelic@0.3.29 + - @backstage/plugin-search-react@1.2.1 + - @backstage/plugin-shortcuts@0.3.3 + - @backstage/plugin-user-settings@0.5.1 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-permission-react@0.4.7 + - @backstage/plugin-search-common@1.1.1 + +## example-backend@0.2.77 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-backend@0.17.1 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/plugin-techdocs-backend@1.4.1 + - @backstage/plugin-scaffolder-backend@1.8.0 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/plugin-playlist-backend@0.2.1 + - @backstage/plugin-azure-devops-backend@0.3.17 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/plugin-code-coverage-backend@0.2.4 + - @backstage/plugin-events-backend@0.1.0 + - @backstage/plugin-events-node@0.1.0 + - @backstage/plugin-kubernetes-backend@0.8.0 + - @backstage/plugin-tech-insights-backend@0.5.4 + - @backstage/plugin-tech-insights-node@0.3.6 + - @backstage/plugin-azure-sites-backend@0.1.0 + - example-app@0.2.77 + - @backstage/plugin-app-backend@0.3.38 + - @backstage/plugin-badges-backend@0.1.32 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/plugin-graphql-backend@0.1.28 + - @backstage/plugin-jenkins-backend@0.1.28 + - @backstage/plugin-kafka-backend@0.2.31 + - @backstage/plugin-permission-backend@0.5.13 + - @backstage/plugin-permission-node@0.7.1 + - @backstage/plugin-proxy-backend@0.2.32 + - @backstage/plugin-rollbar-backend@0.1.35 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.6 + - @backstage/plugin-search-backend@1.1.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.0.4 + - @backstage/plugin-search-backend-module-pg@0.4.2 + - @backstage/plugin-search-backend-node@1.0.4 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.22 + - @backstage/plugin-todo-backend@0.1.35 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/plugin-search-common@1.1.1 + +## example-backend-next@0.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/plugin-scaffolder-backend@1.8.0 + - @backstage/plugin-app-backend@0.3.38 + - @backstage/backend-defaults@0.1.3 + +## techdocs-cli-embedded-app@0.2.76 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0 + - @backstage/core-components@0.12.0 + - @backstage/core-app-api@1.2.0 + - @backstage/cli@0.21.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/plugin-catalog@1.6.1 + - @backstage/app-defaults@1.0.8 + - @backstage/integration-react@1.1.6 + - @backstage/test-utils@1.2.2 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## @internal/plugin-catalog-customized@0.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/plugin-catalog@1.6.1 + +## @internal/plugin-todo-list@1.0.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## @internal/plugin-todo-list-backend@1.0.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## @internal/plugin-todo-list-common@1.0.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.1 diff --git a/docs/releases/v1.8.0-next.1-changelog.md b/docs/releases/v1.8.0-next.1-changelog.md new file mode 100644 index 0000000000..1f65dfdc9c --- /dev/null +++ b/docs/releases/v1.8.0-next.1-changelog.md @@ -0,0 +1,106 @@ +# Release v1.8.0-next.1 + +## @backstage/plugin-scaffolder-backend@1.8.0-next.1 + +### Minor Changes + +- 5921b5ce49: - The GitLab Project ID for the `publish:gitlab:merge-request` action is now passed through the query parameter `project` in the `repoUrl`. It still allows people to not use the `projectid` and use the `repoUrl` with the `owner` and `repo` query parameters instead. This makes it easier to publish to repositories instead of writing the full path to the project. + +## @backstage/create-app@0.4.33-next.1 + +### Patch Changes + +- Bumped create-app version. + +## @backstage/plugin-azure-devops-backend@0.3.17-next.1 + +### Patch Changes + +- 62f284e394: - Adjusted the asset parser to accept case sensitive + - Fixed fetching data that was using the deprecated function + +## @backstage/plugin-playlist@0.1.2-next.1 + +### Patch Changes + +- 605f269f0d: Updated Playlist plugin docs: + + - Updated `playlist` plugin README to include note about installing backend plugin and added images for the various features + - Updated `playlist-backend` plugin README to remove `IdentityClient` import in example as it is not used and made minor change to headings + +## @backstage/plugin-playlist-backend@0.2.1-next.1 + +### Patch Changes + +- 605f269f0d: Updated Playlist plugin docs: + + - Updated `playlist` plugin README to include note about installing backend plugin and added images for the various features + - Updated `playlist-backend` plugin README to remove `IdentityClient` import in example as it is not used and made minor change to headings + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.1 + +## @backstage/plugin-techdocs@1.4.0-next.1 + +### Patch Changes + +- 9e4d8e6198: Fix logic bug that broke techdocs-cli-embedded-app + +## @backstage/plugin-techdocs-addons-test-utils@1.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0-next.1 + +## example-app@0.2.77-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0-next.1 + - @backstage/plugin-playlist@0.1.2-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.6-next.0 + +## example-backend@0.2.77-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.1 + - @backstage/plugin-playlist-backend@0.2.1-next.1 + - @backstage/plugin-azure-devops-backend@0.3.17-next.1 + - example-app@0.2.77-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.6-next.1 + +## example-backend-next@0.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.1 + +## techdocs-cli-embedded-app@0.2.76-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0-next.1 diff --git a/docs/releases/v1.8.0-next.2-changelog.md b/docs/releases/v1.8.0-next.2-changelog.md new file mode 100644 index 0000000000..d6c55998f6 --- /dev/null +++ b/docs/releases/v1.8.0-next.2-changelog.md @@ -0,0 +1,1903 @@ +# Release v1.8.0-next.2 + +## @backstage/cli@0.21.0-next.1 + +### Minor Changes + +- 384eaa2307: Switched `tsconfig.json` to target and support `ES2021`, in line with the bump to Node.js 16 & 18. + +### Patch Changes + +- 88f99b8b13: Bumped `tar` dependency to `^6.1.12` in order to ensure Node.js v18 compatibility. +- 969a8444ea: Updated dependency `esbuild` to `^0.15.0`. +- Updated dependencies + - @backstage/release-manifests@0.0.7-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4-next.0 + - @backstage/config-loader@1.1.6-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-org@0.6.0-next.1 + +### Minor Changes + +- 0b11500151: Updates the User and Group Profile cards to add the links from the UserEntity or the GroupEntity + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-org-react@0.1.0-next.0 + +### Minor Changes + +- e96274f1fe: Implemented the org-react plugin, with it's first component being: a `GroupListPicker` component that will give the user the ability to choose a group + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-scaffolder-backend@1.8.0-next.2 + +### Minor Changes + +- 5025d2e8b6: Adds the ability to pass (an optional) array of strings that will be applied to the newly scaffolded repository as topic labels. + +### Patch Changes + +- 969a8444ea: Updated dependency `esbuild` to `^0.15.0`. +- 9ff4ff3745: Implement "Branch protection rules" support for "publish:github" action +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-scaffolder-common@1.2.2-next.0 + +## @backstage/plugin-splunk-on-call@0.4.0-next.1 + +### Minor Changes + +- 34b772ef31: Use the routing key if it's available instead of team name when triggering incidents. + + BREAKING CHANGE: + Before, the team name was used even if the routing key (with or without team) was used. + Now, the routing key defined for the component will be used instead of the team name. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/app-defaults@1.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.7-next.0 + +## @backstage/backend-app-api@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-permission-node@0.7.1-next.1 + - @backstage/errors@1.1.3-next.0 + +## @backstage/backend-common@0.16.0-next.1 + +### Patch Changes + +- 88f99b8b13: Bumped `tar` dependency to `^6.1.12` in order to ensure Node.js v18 compatibility. +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4-next.0 + - @backstage/config-loader@1.1.6-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/backend-defaults@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.2.3-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + +## @backstage/backend-plugin-api@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + +## @backstage/backend-tasks@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/backend-test-utils@0.1.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/cli@0.21.0-next.1 + - @backstage/backend-app-api@0.2.3-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/config@1.0.4-next.0 + +## @backstage/codemods@0.1.41-next.0 + +### Patch Changes + +- 58502ec285: Updated dependency `jscodeshift` to `^0.14.0`. +- Updated dependencies + - @backstage/cli-common@0.1.10 + +## @backstage/core-components@0.12.0-next.1 + +### Patch Changes + +- b4fb5c8ecc: MissingAnnotationEmptyState now accepts either a string or an array of strings to support multiple missing annotations. +- Updated dependencies + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.1 + +## @backstage/create-app@0.4.34-next.2 + +### Patch Changes + +- 384eaa2307: Switched Node.js version to support version 16 & 18, rather than 14 & 16. To switch the Node.js version in your own project, apply the following change to the root `package.json`: + + ```diff + "engines": { + - "node": "14 || 16" + + "node": "16 || 18" + }, + ``` + + As well as the following change to `packages/app/package.json`: + + ```diff + - "@types/node": "^14.14.32", + + "@types/node": "^16.11.26", + ``` + +- 864c876e57: Fixed incorrect comments in the templated `app-config.yaml` and `app-config.production.yaml`. The `backend.listen` directive is not in fact needed to override the `backend.baseUrl`, the backend listens to all interfaces by default. The configuration has also been updated to listen to all interfaces, rather than just IPv4 ones, as this is required for Node.js v18. The production configuration now also shows the option to specify `backend.listen` as a single string. + + To apply this changes to an existing app, make the following change to `app-config.yaml`: + + ```diff + - # Uncomment the following host directive to bind to all IPv4 interfaces and + - # not just the baseUrl hostname. + - # host: 0.0.0.0 + + # Uncomment the following host directive to bind to specific interfaces + + # host: 127.0.0.1 + ``` + + And the following change to `app-config.production.yaml`: + + ```diff + - listen: + - port: 7007 + - # The following host directive binds to all IPv4 interfaces when its value + - # is "0.0.0.0". This is the most permissive setting. The right value depends + - # on your specific deployment. If you remove the host line entirely, the + - # backend will bind on the interface that corresponds to the backend.baseUrl + - # hostname. + - host: 0.0.0.0 + + # The listener can also be expressed as a single : string. In this case we bind to + + # all interfaces, the most permissive setting. The right value depends on your specific deployment. + + listen: ':7007' + ``` + +- Updated dependencies + - @backstage/cli-common@0.1.10 + +## @backstage/dev-utils@1.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/app-defaults@1.0.8-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/test-utils@1.2.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/integration-react@1.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + +## @backstage/release-manifests@0.0.7-next.0 + +### Patch Changes + +- a4496131fa: Added a fallback that fetches manifests from `https://raw.githubusercontent.com` if `https://versions.backstage.io` is unavailable. + +## @techdocs/cli@1.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-techdocs-node@1.4.2-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4-next.0 + +## @backstage/plugin-adr@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.3-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + +## @backstage/plugin-adr-backend@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/plugin-adr-common@0.2.3-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-airbrake@0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/dev-utils@1.0.8-next.1 + - @backstage/test-utils@1.2.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-airbrake-backend@0.2.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + +## @backstage/plugin-allure@0.1.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-analytics-module-ga@0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apache-airflow@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + +## @backstage/plugin-api-docs@0.8.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog@1.6.1-next.1 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-apollo-explorer@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-app-backend@0.3.38-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/config-loader@1.1.6-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-auth-backend@0.17.1-next.1 + +### Patch Changes + +- 0d6837ca4e: Fix wrong GitHub callback URL documentation +- abaed9770e: Improve logging +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-auth-node@0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + +## @backstage/plugin-azure-devops@0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-azure-devops-backend@0.3.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-azure-sites-backend@0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-azure-sites-common@0.1.0-next.0 + +## @backstage/plugin-badges@0.2.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-badges-backend@0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + +## @backstage/plugin-bazaar@0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.21.0-next.1 + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-catalog@1.6.1-next.1 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-bazaar-backend@0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-test-utils@0.1.30-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + +## @backstage/plugin-bitrise@0.1.38-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-catalog@1.6.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + +## @backstage/plugin-catalog-backend@1.5.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/plugin-permission-node@0.7.1-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-scaffolder-common@1.2.2-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.1-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.1-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.4.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-catalog-graph@0.2.23-next.1 + +### Patch Changes + +- da0bf25d1a: Preserve graph options and increment `maxDepth` by 1. + + The change will preserve options used at the `CatalogGraphCard` + (displayed at the entity page) and additionally, increments the + `maxDepth` option by 1 to increase the scope slightly compared to + the graph already seen by the users. + + The default for `maxDepth` at `CatalogGraphCard` is 1. + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-catalog-import@0.9.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-catalog-node@1.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-catalog-common@1.0.8-next.0 + +## @backstage/plugin-catalog-react@1.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/version-bridge@1.0.1 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-permission-react@0.4.7-next.0 + +## @backstage/plugin-cicd-statistics@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/plugin-cicd-statistics@0.1.13-next.1 + +## @backstage/plugin-circleci@0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-cloudbuild@0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-code-climate@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-code-coverage@0.2.4-next.1 + +### Patch Changes + +- fcab2579a0: Adds installation instructions +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-code-coverage-backend@0.2.4-next.1 + +### Patch Changes + +- fcab2579a0: Adds installation instructions +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + +## @backstage/plugin-codescene@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-config-schema@0.1.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-cost-insights@0.12.0-next.1 + +### Patch Changes + +- e92aa15f01: Bumped `canvas` dependency to the latest version, which has better Node.js v18 support. +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-dynatrace@1.0.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-explore@0.3.42-next.1 + +### Patch Changes + +- 5c25ce6d9e: Added a section to explore plugin README that describes the customization of explore tools content. +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-explore-react@0.0.23-next.0 + +## @backstage/plugin-firehydrant@0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-fossa@0.2.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-gcalendar@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcp-projects@0.3.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-git-release-manager@0.3.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-actions@0.5.11-next.1 + +### Patch Changes + +- ed438a3ba5: Add error panel when the plugin fails. +- 0d6837ca4e: Fix wrong GitHub callback URL documentation +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-github-deployments@0.1.42-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-github-issues@0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-github-pull-requests-board@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-gitops-profiles@0.3.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gocd@0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-graphiql@0.2.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphql-backend@0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-catalog-graphql@0.3.15-next.0 + +## @backstage/plugin-home@0.4.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-stack-overflow@0.1.7-next.1 + +## @backstage/plugin-ilert@0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-jenkins@0.7.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-jenkins-common@0.1.10-next.0 + +## @backstage/plugin-jenkins-backend@0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-jenkins-common@0.1.10-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + +## @backstage/plugin-kafka@0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-kafka-backend@0.2.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + +## @backstage/plugin-kubernetes@0.7.4-next.1 + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/plugin-kubernetes-common@0.4.4-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-kubernetes-backend@0.8.0-next.1 + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-kubernetes-common@0.4.4-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + +## @backstage/plugin-kubernetes-common@0.4.4-next.1 + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/catalog-model@1.1.3-next.0 + +## @backstage/plugin-lighthouse@0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-newrelic@0.3.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic-dashboard@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-pagerduty@0.5.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-periskop@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-periskop-backend@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + +## @backstage/plugin-permission-backend@0.5.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/plugin-permission-node@0.7.1-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + +## @backstage/plugin-permission-node@0.7.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + +## @backstage/plugin-playlist@0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-permission-react@0.4.7-next.0 + - @backstage/plugin-playlist-common@0.1.2-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + +## @backstage/plugin-playlist-backend@0.2.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-test-utils@0.1.30-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/plugin-permission-node@0.7.1-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-playlist-common@0.1.2-next.0 + +## @backstage/plugin-proxy-backend@0.2.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + +## @backstage/plugin-rollbar@0.4.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-rollbar-backend@0.1.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + +## @backstage/plugin-scaffolder@1.8.0-next.1 + +### Patch Changes + +- 580285787d: The `create` and `click` analytics events are now also captured on the "next" version of the component creation page. +- 3b3fc3cc3c: Fix `formData` not being present in the `next` version +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-permission-react@0.4.7-next.0 + - @backstage/plugin-scaffolder-common@1.2.2-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-scaffolder-backend@1.8.0-next.2 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-scaffolder-backend@1.8.0-next.2 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.2 + - @backstage/config@1.0.4-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-search@1.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/version-bridge@1.0.1 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + +## @backstage/plugin-search-backend@1.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/plugin-permission-node@0.7.1-next.1 + - @backstage/plugin-search-backend-node@1.0.4-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-search-backend-module-elasticsearch@1.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.4-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-search-backend-module-pg@0.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-search-backend-node@1.0.4-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-search-backend-node@1.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-search-react@1.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/version-bridge@1.0.1 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-sentry@0.4.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-shortcuts@0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-sonarqube@0.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-sonarqube-backend@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + +## @backstage/plugin-stack-overflow@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-home@0.4.27-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-stack-overflow-backend@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.21.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-tech-insights@0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-tech-insights-common@0.2.8-next.0 + +## @backstage/plugin-tech-insights-backend@0.5.4-next.1 + +### Patch Changes + +- f12e9e5b8c: Add Documentation on 404 Errors +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-tech-insights-node@0.3.6-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-tech-insights-common@0.2.8-next.0 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-tech-insights-node@0.3.6-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-tech-insights-common@0.2.8-next.0 + +## @backstage/plugin-tech-insights-node@0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-tech-insights-common@0.2.8-next.0 + +## @backstage/plugin-tech-radar@0.5.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs@1.4.0-next.2 + +### Patch Changes + +- e92aa15f01: Bumped `canvas` dependency to the latest version, which has better Node.js v18 support. +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0-next.2 + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/test-utils@1.2.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog@1.6.1-next.1 + - @backstage/plugin-search-react@1.2.1-next.1 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + +## @backstage/plugin-techdocs-backend@1.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-techdocs-node@1.4.2-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + +## @backstage/plugin-techdocs-node@1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + +## @backstage/plugin-techdocs-react@1.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/version-bridge@1.0.1 + +## @backstage/plugin-todo@0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-todo-backend@0.1.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + +## @backstage/plugin-user-settings@0.5.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-user-settings-backend@0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + +## @backstage/plugin-vault@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @backstage/plugin-vault-backend@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/backend-test-utils@0.1.30-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + +## @backstage/plugin-xcmetrics@0.2.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + +## example-app@0.2.77-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.8.0-next.1 + - @backstage/plugin-cost-insights@0.12.0-next.1 + - @backstage/plugin-techdocs@1.4.0-next.2 + - @backstage/plugin-explore@0.3.42-next.1 + - @backstage/plugin-github-actions@0.5.11-next.1 + - @backstage/cli@0.21.0-next.1 + - @backstage/plugin-catalog-graph@0.2.23-next.1 + - @backstage/core-components@0.12.0-next.1 + - @backstage/plugin-code-coverage@0.2.4-next.1 + - @backstage/plugin-org@0.6.0-next.1 + - @backstage/plugin-kubernetes@0.7.4-next.1 + - @backstage/app-defaults@1.0.8-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-airbrake@0.3.11-next.1 + - @backstage/plugin-apache-airflow@0.2.4-next.1 + - @backstage/plugin-api-docs@0.8.11-next.1 + - @backstage/plugin-azure-devops@0.2.2-next.1 + - @backstage/plugin-azure-sites@0.1.0-next.1 + - @backstage/plugin-badges@0.2.35-next.1 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-catalog-import@0.9.1-next.1 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-circleci@0.3.11-next.1 + - @backstage/plugin-cloudbuild@0.3.11-next.1 + - @backstage/plugin-dynatrace@1.0.1-next.1 + - @backstage/plugin-gcalendar@0.3.7-next.1 + - @backstage/plugin-gcp-projects@0.3.30-next.1 + - @backstage/plugin-gocd@0.1.17-next.1 + - @backstage/plugin-graphiql@0.2.43-next.1 + - @backstage/plugin-home@0.4.27-next.1 + - @backstage/plugin-jenkins@0.7.10-next.1 + - @backstage/plugin-kafka@0.3.11-next.1 + - @backstage/plugin-lighthouse@0.3.11-next.1 + - @backstage/plugin-newrelic@0.3.29-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.4-next.1 + - @backstage/plugin-pagerduty@0.5.4-next.1 + - @backstage/plugin-permission-react@0.4.7-next.0 + - @backstage/plugin-playlist@0.1.2-next.2 + - @backstage/plugin-rollbar@0.4.11-next.1 + - @backstage/plugin-search@1.0.4-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + - @backstage/plugin-sentry@0.4.4-next.1 + - @backstage/plugin-shortcuts@0.3.3-next.1 + - @backstage/plugin-stack-overflow@0.1.7-next.1 + - @backstage/plugin-tech-insights@0.3.3-next.1 + - @backstage/plugin-tech-radar@0.5.18-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.6-next.1 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + - @backstage/plugin-todo@0.2.13-next.1 + - @backstage/plugin-user-settings@0.5.1-next.1 + - @internal/plugin-catalog-customized@0.0.4-next.1 + +## example-backend@0.2.77-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.17.1-next.1 + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-scaffolder-backend@1.8.0-next.2 + - @backstage/plugin-code-coverage-backend@0.2.4-next.1 + - @backstage/plugin-kubernetes-backend@0.8.0-next.1 + - @backstage/plugin-tech-insights-backend@0.5.4-next.1 + - example-app@0.2.77-next.2 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-app-backend@0.3.38-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/plugin-azure-devops-backend@0.3.17-next.2 + - @backstage/plugin-azure-sites-backend@0.1.0-next.1 + - @backstage/plugin-badges-backend@0.1.32-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-graphql-backend@0.1.28-next.1 + - @backstage/plugin-jenkins-backend@0.1.28-next.1 + - @backstage/plugin-kafka-backend@0.2.31-next.1 + - @backstage/plugin-permission-backend@0.5.13-next.1 + - @backstage/plugin-permission-node@0.7.1-next.1 + - @backstage/plugin-playlist-backend@0.2.1-next.2 + - @backstage/plugin-proxy-backend@0.2.32-next.1 + - @backstage/plugin-rollbar-backend@0.1.35-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.6-next.2 + - @backstage/plugin-search-backend@1.1.1-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.0.4-next.1 + - @backstage/plugin-search-backend-module-pg@0.4.2-next.1 + - @backstage/plugin-search-backend-node@1.0.4-next.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.22-next.1 + - @backstage/plugin-tech-insights-node@0.3.6-next.1 + - @backstage/plugin-techdocs-backend@1.4.1-next.1 + - @backstage/plugin-todo-backend@0.1.35-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + +## example-backend-next@0.0.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.2 + - @backstage/plugin-app-backend@0.3.38-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/backend-defaults@0.1.3-next.1 + +## techdocs-cli-embedded-app@0.2.76-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0-next.2 + - @backstage/cli@0.21.0-next.1 + - @backstage/core-components@0.12.0-next.1 + - @backstage/app-defaults@1.0.8-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/test-utils@1.2.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog@1.6.1-next.1 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + +## @internal/plugin-catalog-customized@0.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.6.1-next.1 + - @backstage/plugin-catalog-react@1.2.1-next.1 + +## @internal/plugin-todo-list@1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + +## @internal/plugin-todo-list-backend@1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 diff --git a/docs/releases/v1.8.0.md b/docs/releases/v1.8.0.md new file mode 100644 index 0000000000..cbe16a0cba --- /dev/null +++ b/docs/releases/v1.8.0.md @@ -0,0 +1,78 @@ +--- +id: v1.8.0 +title: v1.8.0 +description: Backstage Release v1.8.0 +--- + +These are the release notes for the v1.8.0 release of [Backstage](https://backstage.io/). + +A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done. + +## Highlights + +### Node 16 and 18 + +The project was updated to support and build against Node versions 16 and 18, after 18 had been elevated to [active LTS](https://github.com/nodejs/release#release-schedule). This means that support for Node 14 has been dropped in accordance with [our versioning policy](https://backstage.io/docs/overview/versioning-policy#nodejs-releases). + +We would like to call out one particular change in Node 18 that may affect adopters. The way that it binds interfaces internally when listening for incoming connections has changed such that it may prefer e.g. an IPv6 interface that you did not intend to use. This might make it seem like the backend is unreachable from the outside in some circumstances. Because of this, you may want to change your configuration to say something along the lines of `listen: ':7007'` under the `backend` section to ensure that it listens on all interfaces. + +### Events + +An early version of the `events` plugin ecosystem has been added. This allows events such as webhooks and similar to be received and propagated by a Backstage backend, and plugins can then react to those events. One early use case for this plugin is to start powering catalog updates. + +This is early days, so bear in mind that interfaces and implementations may still be subject to change. But do check it out, and let us know what you think! + +Contributed by [@pjungermann](https://github.com/pjungermann) in [#13931](https://github.com/backstage/backstage/pull/13931) + +### Gitea + +There is now support for [Gitea](https://gitea.io) as a general SCM integration. Please check out [the docs](https://backstage.io/docs/integrations/gitea/locations) for how to get started. + +Contributed by [@atoko](https://github.com/atoko) in [#14056](https://github.com/backstage/backstage/pull/14056) + +### Azure Sites + +There's a new Azure Sites (Apps & Functions) plugin. It lets you view the current status of the site, and quickly jump to the site's Overview or Log Stream pages. Please check out [the plugin README](https://github.com/backstage/backstage/tree/master/plugins/azure-sites) for more information. + +Contributed by [@wesley-pattison](https://github.com/wesley-pattison) in [#13634](https://github.com/backstage/backstage/pull/13634) + +### Scaffolder Metrics + +The scaffolder backend now emits default Prometheus metrics that track job execution. + +Contributed by [@spencerrichardhenry](https://github.com/spencerrichardhenry) in [#13815](https://github.com/backstage/backstage/pull/13815) + +### `@backstage/backend-common` + +The `UrlReader` interface has been updated to require that `readUrl` is implemented. The `readUrl` method has previously been optional to implement, but a warning used to be logged when calling its predecessor `read`. + +The `read` method is now deprecated and will be removed in a future release, and if you made custom URL readers you will have to implement a `readUrl` method on them. + +### Recharts + +The `recharts` library that powers the graphing functionality in some plugins was upgraded across the repository to version 2. Please let us know if you run into any new issues with graphing in plugins such as Bitrise, CICD statistics, code coverage, cost insights, GIT release manager, or XCmetrics. + +### `GitHub` to `Github` + +We are pursuing an effort to standardize the naming of exported GitHub related symbols. After an upgrade of Backstage, you may therefore see errors related to not finding components and types whose names start with “GitHub”. As an example, if you are using `GitHubIssuesPage` from the GitHub Issues plugin, you now need to import and use it as `GithubIssuesPage` instead. + +## Security Fixes + +This release does not contain any security fixes. + +## Upgrade path + +We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). + +## Links and References + +Below you can find a list of links and references to help you learn about and start using this new release. + +- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) +- [GitHub repository](https://github.com/backstage/backstage) +- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) +- [Community Discord](https://discord.gg/bFESRKVt) for discussions and support +- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.8.0-changelog.md) +- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) + +Sign up for our [newsletter](https://mailchi.mp/spotify/backstage-community) if you want to be informed about what is happening in the world of Backstage. diff --git a/docs/releases/v1.9.0-next.0-changelog.md b/docs/releases/v1.9.0-next.0-changelog.md new file mode 100644 index 0000000000..690bf60ceb --- /dev/null +++ b/docs/releases/v1.9.0-next.0-changelog.md @@ -0,0 +1,2305 @@ +# Release v1.9.0-next.0 + +## @backstage/catalog-client@1.2.0-next.0 + +### Minor Changes + +- 00d90b520a: **BREAKING PRODUCERS**: Added a new `getEntitiesByRefs` endpoint to `CatalogApi`, for efficient batch fetching of entities by ref. + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/repo-tools@0.1.0-next.0 + +### Minor Changes + +- 99713fd671: Introducing repo-tools package + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-catalog-backend@1.6.0-next.0 + +### Minor Changes + +- 16891a212c: Added new `POST /entities/by-refs` endpoint, which allows you to efficiently + batch-fetch entities by their entity ref. This can be useful e.g. in graphql + resolvers or similar contexts where you need to fetch many entities at the same + time. + +### Patch Changes + +- d8593ce0e6: Do not use deprecated `LocationSpec` from the `@backstage/plugin-catalog-node` package +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- e982f77fe3: Registered shutdown hook in experimental catalog plugin. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-permission-node@0.7.2-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-scaffolder-common@1.2.3-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-events-backend@0.2.0-next.0 + +### Minor Changes + +- cf41eedf43: **BREAKING:** Remove required field `router` at `HttpPostIngressEventPublisher.fromConfig` + and replace it with `bind(router: Router)`. + Additionally, the path prefix `/http` will be added inside `HttpPostIngressEventPublisher`. + + ```diff + // at packages/backend/src/plugins/events.ts + const eventsRouter = Router(); + - const httpRouter = Router(); + - eventsRouter.use('/http', httpRouter); + + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + logger: env.logger, + - router: httpRouter, + }); + + http.bind(eventsRouter); + ``` + +### Patch Changes + +- cf41eedf43: Introduce a new interface `RequestDetails` to abstract `Request` + providing access to request body and headers. + + **BREAKING:** Replace `request: Request` with `request: RequestDetails` at `RequestValidator`. + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-events-node@0.2.0-next.0 + +### Minor Changes + +- cf41eedf43: Introduce a new interface `RequestDetails` to abstract `Request` + providing access to request body and headers. + + **BREAKING:** Replace `request: Request` with `request: RequestDetails` at `RequestValidator`. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.0 + +## @backstage/plugin-scaffolder@1.9.0-next.0 + +### Minor Changes + +- ddd1c3308d: Implement Custom Field Explorer to view and play around with available installed custom field extensions +- adb1b01e32: Adds the ability to supply a `transformErrors` function to the `Stepper` for `/next` + +### Patch Changes + +- d4d07cf55e: Enabling the customization of the last step in the scaffolder template. + + To override the content you have to do the next: + + ```typescript jsx + + ``` + +- ef803022f1: Initialize all `formData` in the `Stepper` in `/next` + +- 3280711113: Updated dependency `msw` to `^0.49.0`. + +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. + +- a63e2df559: fixed `headerOptions` not passed to `TemplatePage` component + +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-permission-react@0.4.8-next.0 + - @backstage/plugin-scaffolder-common@1.2.3-next.0 + +## @backstage/plugin-user-settings@0.6.0-next.0 + +### Minor Changes + +- 29bdda5442: Added the ability to fully customize settings page. Deprecated UserSettingsTab in favour of SettingsLayout.Route + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/app-defaults@1.0.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.8-next.0 + +## @backstage/backend-app-api@0.2.4-next.0 + +### Patch Changes + +- d6dbf1792b: Added `lifecycleFactory` implementation. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-permission-node@0.7.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/backend-common@0.16.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- dfc8edf9c5: Internal refactor to avoid usage of deprecated symbols. +- Updated dependencies + - @backstage/config-loader@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/backend-defaults@0.1.4-next.0 + +### Patch Changes + +- d6dbf1792b: Added `lifecycleFactory` to default service factories. +- Updated dependencies + - @backstage/backend-app-api@0.2.4-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## @backstage/backend-plugin-api@0.1.5-next.0 + +### Patch Changes + +- d6dbf1792b: Added initial support for registering shutdown hooks via `lifecycleServiceRef`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/backend-tasks@0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/backend-test-utils@0.1.31-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/cli@0.21.2-next.0 + - @backstage/backend-app-api@0.2.4-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/catalog-model@1.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/cli@0.21.2-next.0 + +### Patch Changes + +- 91d050c140: changed tests created by create-plugin to follow eslint-rules best practices particularly testing-library/prefer-screen-queries and testing-library/render-result-naming-convention +- 459a3457e1: Bump `msw` version in default plugin/app templates +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/config-loader@1.1.7-next.0 + - @backstage/release-manifests@0.0.8-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/config@1.0.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.0 + +## @backstage/config-loader@1.1.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/core-app-api@1.2.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/version-bridge@1.0.2 + +## @backstage/core-components@0.12.1-next.0 + +### Patch Changes + +- ea4a5be8f3: Create a variable for minimum height and add a prop named 'fit' for determining if the graph height should grow or be contained. +- 64a579a998: Add items prop to SupportButton. This prop can be used to override the items that would otherwise be grabbed from the config. +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- 17a8e32f39: Updated dependency `rc-progress` to `3.4.1`. +- dfc8edf9c5: Internal refactor to avoid usage of deprecated symbols. +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.2 + +## @backstage/core-plugin-api@1.1.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/version-bridge@1.0.2 + +## @backstage/create-app@0.4.35-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.10 + +## @backstage/dev-utils@1.0.9-next.0 + +### Patch Changes + +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/test-utils@1.2.3-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/app-defaults@1.0.9-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/errors@1.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.0 + +## @backstage/integration@1.4.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 34b039ca9f: Added `integrations.github.apps.allowedInstallationOwners` to the configuration schema. +- Updated dependencies + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/integration-react@1.1.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## @backstage/release-manifests@0.0.8-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. + +## @techdocs/cli@1.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-techdocs-node@1.4.3-next.0 + +## @backstage/test-utils@1.2.3-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.8-next.0 + +## @backstage/types@1.0.2-next.0 + +### Patch Changes + +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. + +## @backstage/plugin-adr@0.2.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## @backstage/plugin-adr-backend@0.2.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-adr-common@0.2.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-adr-common@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.4.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-airbrake@0.3.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/test-utils@1.2.3-next.0 + - @backstage/dev-utils@1.0.9-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-airbrake-backend@0.2.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-allure@0.1.28-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-analytics-module-ga@0.1.23-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apache-airflow@0.2.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + +## @backstage/plugin-api-docs@0.8.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/plugin-catalog@1.6.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apollo-explorer@0.1.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-app-backend@0.3.39-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config-loader@1.1.7-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-auth-backend@0.17.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-auth-node@0.2.8-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-azure-devops@0.2.3-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-devops-backend@0.3.18-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-azure-sites-backend@0.1.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.36-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-badges-backend@0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-bazaar@0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/cli@0.21.2-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/plugin-catalog@1.6.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-bazaar-backend@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-test-utils@0.1.31-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-bitbucket-cloud-common@0.2.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/integration@1.4.1-next.0 + +## @backstage/plugin-bitrise@0.1.39-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-catalog@1.6.2-next.0 + +### Patch Changes + +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- 387d1d5218: Fixed Entity kind pluralisation in the `CatalogKindHeader` component. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.10-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.6-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.2-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.6-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-catalog-backend-module-github@0.2.2-next.0 + +### Patch Changes + +- 70fa5ec3ec: Fixes the assignment of group member references in `GithubMultiOrgProcessor` so membership relations are resolved correctly. +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 754b5854df: Fix incorrectly exported GithubOrgEntityProvider as a type +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.10-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.4.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-catalog-common@1.0.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-catalog-graph@0.2.24-next.0 + +### Patch Changes + +- cb716004ef: Internal refactor to improve tests +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-catalog-graphql@0.3.16-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-catalog-import@0.9.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + +## @backstage/plugin-catalog-node@1.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + +## @backstage/plugin-catalog-react@1.2.2-next.0 + +### Patch Changes + +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.2 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-permission-react@0.4.8-next.0 + +## @backstage/plugin-cicd-statistics@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/plugin-cicd-statistics@0.1.14-next.0 + +## @backstage/plugin-circleci@0.3.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-cloudbuild@0.3.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-climate@0.1.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-coverage@0.2.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-coverage-backend@0.2.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-codescene@0.1.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 17a8e32f39: Updated dependency `rc-progress` to `3.4.1`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-config-schema@0.1.35-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-cost-insights@0.12.1-next.0 + +### Patch Changes + +- f9bbb3be37: Provide the ability to change the base currency from USD to any other currency in cost insights plugin +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-dynatrace@1.0.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-events-backend-module-aws-sqs@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-events-backend-module-azure@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## @backstage/plugin-events-backend-module-github@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## @backstage/plugin-events-backend-module-gitlab@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## @backstage/plugin-events-backend-test-utils@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + +## @backstage/plugin-explore@0.3.43-next.0 + +### Patch Changes + +- ea4a5be8f3: Adds styling to graph forcing it to always fill out the available space. +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-explore-react@0.0.24-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-explore-react@0.0.24-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.0 + +## @backstage/plugin-firehydrant@0.1.29-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-fossa@0.2.44-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcalendar@0.3.8-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcp-projects@0.3.31-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-git-release-manager@0.3.25-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-actions@0.5.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-deployments@0.1.43-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-issues@0.2.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-pull-requests-board@0.1.6-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gitops-profiles@0.3.30-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gocd@0.1.18-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphiql@0.2.44-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphql-backend@0.1.29-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-catalog-graphql@0.3.16-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-home@0.4.28-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-stack-overflow@0.1.8-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-ilert@0.2.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-jenkins@0.7.11-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-jenkins-common@0.1.11-next.0 + +## @backstage/plugin-jenkins-backend@0.1.29-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-jenkins-common@0.1.11-next.0 + +## @backstage/plugin-jenkins-common@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + +## @backstage/plugin-kafka@0.3.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-kafka-backend@0.2.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-kubernetes@0.7.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/plugin-kubernetes-common@0.4.5-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-kubernetes-backend@0.8.1-next.0 + +### Patch Changes + +- b585179770: Added Kubernetes proxy API route to backend Kubernetes plugin, allowing Backstage plugin developers to read/write new information from Kubernetes (if proper credentials are provided). +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-test-utils@0.1.31-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-kubernetes-common@0.4.5-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-kubernetes-common@0.4.5-next.0 + +### Patch Changes + +- b585179770: Added Kubernetes proxy API route to backend Kubernetes plugin, allowing Backstage plugin developers to read/write new information from Kubernetes (if proper credentials are provided). +- Updated dependencies + - @backstage/catalog-model@1.1.4-next.0 + +## @backstage/plugin-lighthouse@0.3.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic@0.3.30-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic-dashboard@0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-org@0.6.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-org-react@0.1.1-next.0 + +### Patch Changes + +- 4cb5066828: Bug fixes and adding the possibility to add a default value for the `GroupListPicker`. Fixes: Vertical size jump on text entry, left align for text, selecting a value closes the popup, auto focus on the popup when opening +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-pagerduty@0.5.5-next.0 + +### Patch Changes + +- cb716004ef: Internal refactor to improve tests +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-periskop@0.1.10-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-periskop-backend@0.1.10-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-permission-backend@0.5.14-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-permission-node@0.7.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-permission-common@0.7.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-permission-node@0.7.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-permission-react@0.4.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-playlist@0.1.3-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-permission-react@0.4.8-next.0 + - @backstage/plugin-playlist-common@0.1.3-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## @backstage/plugin-playlist-backend@0.2.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-test-utils@0.1.31-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-permission-node@0.7.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-playlist-common@0.1.3-next.0 + +## @backstage/plugin-playlist-common@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.0 + +## @backstage/plugin-proxy-backend@0.2.33-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-rollbar@0.4.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-rollbar-backend@0.1.36-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-scaffolder-backend@1.8.1-next.0 + +### Patch Changes + +- cb716004ef: Internal refactor to improve tests +- 26404430bc: Use Json types from @backstage/types +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-scaffolder-common@1.2.3-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.14-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.1-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.1-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + +## @backstage/plugin-scaffolder-common@1.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + +## @backstage/plugin-search@1.0.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.2 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## @backstage/plugin-search-backend@1.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.5-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-permission-node@0.7.2-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-search-backend-module-elasticsearch@1.0.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.5-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-search-backend-module-pg@0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.5-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-search-backend-node@1.0.5-next.0 + +### Patch Changes + +- a962ce0551: Wait for indexer initialization before finalizing indexing. +- 683ced83f6: Fixed a bug that could cause a `max listeners exceeded warning` to be logged when more than 10 collators were running simultaneously. +- 81b1e7b0fe: Updated indexer and decorator base classes to take advantage of features introduced in Node.js v16; be sure you are running a [supported version of Node.js](https://backstage.io/docs/releases/v1.8.0#node-16-and-18). +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-search-common@1.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/types@1.0.2-next.0 + +## @backstage/plugin-search-react@1.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.2 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-sentry@0.4.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-shortcuts@0.3.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-sonarqube@0.5.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 17a8e32f39: Updated dependency `rc-progress` to `3.4.1`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-sonarqube-backend@0.1.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-splunk-on-call@0.4.1-next.0 + +### Patch Changes + +- cb716004ef: Internal refactor to improve tests +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-stack-overflow@0.1.8-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-home@0.4.28-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-stack-overflow-backend@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.21.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-tech-insights@0.3.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-tech-insights-common@0.2.9-next.0 + +## @backstage/plugin-tech-insights-backend@0.5.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-tech-insights-common@0.2.9-next.0 + - @backstage/plugin-tech-insights-node@0.3.7-next.0 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-tech-insights-common@0.2.9-next.0 + - @backstage/plugin-tech-insights-node@0.3.7-next.0 + +## @backstage/plugin-tech-insights-common@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.0 + +## @backstage/plugin-tech-insights-node@0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-tech-insights-common@0.2.9-next.0 + +## @backstage/plugin-tech-radar@0.5.19-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs@1.4.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/test-utils@1.2.3-next.0 + - @backstage/plugin-techdocs@1.4.1-next.0 + - @backstage/plugin-catalog@1.6.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-react@1.2.2-next.0 + +## @backstage/plugin-techdocs-backend@1.4.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-techdocs-node@1.4.3-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 8536e7c281: Use `app.title` from `app-config.yaml` when creating new Documentation Feedback issue. `Backstage` is the default value. +- Updated dependencies + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs-node@1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## @backstage/plugin-techdocs-react@1.0.7-next.0 + +### Patch Changes + +- cb716004ef: Internal refactor to improve tests +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/version-bridge@1.0.2 + +## @backstage/plugin-todo@0.2.14-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-todo-backend@0.1.36-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-user-settings-backend@0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-vault@0.1.6-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-vault-backend@0.2.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 7a3d2688ed: Use `express-promise-router` to catch errors properly. + Add `403` error as a known one. It will now return a `NotAllowed` error. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-test-utils@0.1.31-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @backstage/plugin-xcmetrics@0.2.32-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## example-app@0.2.78-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-graph@0.2.24-next.0 + - @backstage/plugin-pagerduty@0.5.5-next.0 + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/plugin-scaffolder@1.9.0-next.0 + - @backstage/cli@0.21.2-next.0 + - @backstage/plugin-cost-insights@0.12.1-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/plugin-user-settings@0.6.0-next.0 + - @backstage/plugin-explore@0.3.43-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/plugin-airbrake@0.3.12-next.0 + - @backstage/plugin-apache-airflow@0.2.5-next.0 + - @backstage/plugin-api-docs@0.8.12-next.0 + - @backstage/plugin-azure-devops@0.2.3-next.0 + - @backstage/plugin-azure-sites@0.1.1-next.0 + - @backstage/plugin-badges@0.2.36-next.0 + - @backstage/plugin-catalog-import@0.9.2-next.0 + - @backstage/plugin-circleci@0.3.12-next.0 + - @backstage/plugin-cloudbuild@0.3.12-next.0 + - @backstage/plugin-code-coverage@0.2.5-next.0 + - @backstage/plugin-dynatrace@1.0.2-next.0 + - @backstage/plugin-gcalendar@0.3.8-next.0 + - @backstage/plugin-gcp-projects@0.3.31-next.0 + - @backstage/plugin-github-actions@0.5.12-next.0 + - @backstage/plugin-gocd@0.1.18-next.0 + - @backstage/plugin-graphiql@0.2.44-next.0 + - @backstage/plugin-home@0.4.28-next.0 + - @backstage/plugin-jenkins@0.7.11-next.0 + - @backstage/plugin-kafka@0.3.12-next.0 + - @backstage/plugin-kubernetes@0.7.5-next.0 + - @backstage/plugin-lighthouse@0.3.12-next.0 + - @backstage/plugin-newrelic@0.3.30-next.0 + - @backstage/plugin-org@0.6.1-next.0 + - @backstage/plugin-playlist@0.1.3-next.0 + - @backstage/plugin-rollbar@0.4.12-next.0 + - @backstage/plugin-search@1.0.5-next.0 + - @backstage/plugin-sentry@0.4.5-next.0 + - @backstage/plugin-shortcuts@0.3.4-next.0 + - @backstage/plugin-stack-overflow@0.1.8-next.0 + - @backstage/plugin-tech-insights@0.3.4-next.0 + - @backstage/plugin-tech-radar@0.5.19-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.7-next.0 + - @backstage/plugin-techdocs@1.4.1-next.0 + - @backstage/plugin-todo@0.2.14-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/app-defaults@1.0.9-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.5-next.0 + - @backstage/plugin-permission-react@0.4.8-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + - @internal/plugin-catalog-customized@0.0.5-next.0 + +## example-backend@0.2.78-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.1-next.0 + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/plugin-events-backend@0.2.0-next.0 + - @backstage/plugin-search-backend-node@1.0.5-next.0 + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-app-backend@0.3.39-next.0 + - @backstage/plugin-auth-backend@0.17.2-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-azure-devops-backend@0.3.18-next.0 + - @backstage/plugin-azure-sites-backend@0.1.1-next.0 + - @backstage/plugin-code-coverage-backend@0.2.5-next.0 + - @backstage/plugin-graphql-backend@0.1.29-next.0 + - @backstage/plugin-jenkins-backend@0.1.29-next.0 + - @backstage/plugin-permission-backend@0.5.14-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-permission-node@0.7.2-next.0 + - @backstage/plugin-playlist-backend@0.2.2-next.0 + - @backstage/plugin-proxy-backend@0.2.33-next.0 + - @backstage/plugin-rollbar-backend@0.1.36-next.0 + - @backstage/plugin-techdocs-backend@1.4.2-next.0 + - @backstage/plugin-todo-backend@0.1.36-next.0 + - @backstage/plugin-kubernetes-backend@0.8.1-next.0 + - example-app@0.2.78-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.7-next.0 + - @backstage/plugin-badges-backend@0.1.33-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/plugin-tech-insights-backend@0.5.5-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-kafka-backend@0.2.32-next.0 + - @backstage/plugin-search-backend@1.1.2-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.0.5-next.0 + - @backstage/plugin-search-backend-module-pg@0.4.3-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.23-next.0 + - @backstage/plugin-tech-insights-node@0.3.7-next.0 + +## example-backend-next@0.0.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.1-next.0 + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/plugin-app-backend@0.3.39-next.0 + - @backstage/backend-defaults@0.1.4-next.0 + +## techdocs-cli-embedded-app@0.2.77-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/cli@0.21.2-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/test-utils@1.2.3-next.0 + - @backstage/plugin-techdocs@1.4.1-next.0 + - @backstage/plugin-catalog@1.6.2-next.0 + - @backstage/app-defaults@1.0.9-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## @internal/plugin-catalog-customized@0.0.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/plugin-catalog@1.6.2-next.0 + +## @internal/plugin-todo-list@1.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## @internal/plugin-todo-list-backend@1.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## @internal/plugin-todo-list-common@1.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.0 diff --git a/docs/releases/v1.9.0-next.1-changelog.md b/docs/releases/v1.9.0-next.1-changelog.md new file mode 100644 index 0000000000..862734131a --- /dev/null +++ b/docs/releases/v1.9.0-next.1-changelog.md @@ -0,0 +1,2325 @@ +# Release v1.9.0-next.1 + +## @backstage/backend-common@0.17.0-next.1 + +### Minor Changes + +- de8a975911: Changed to use native `AbortController` and `AbortSignal` from Node.js, instead + of the one from `node-abort-controller`. This is possible now that the minimum + supported Node.js version of the project is 16. + + Note that their interfaces are very slightly different, but typically not in a + way that matters to consumers. If you see any typescript errors as a direct + result from this, they are compatible with each other in the ways that we + interact with them, and should be possible to type-cast across without ill + effects. + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- 1f2b2de3fe: exported KubernetesContainerRunner, KubernetesContainerRunnerOptions, KubernetesContainerRunnerMountBase +- 6b82598bd8: Added the ability to understand Job Artifact URLs to the GitLab integration +- 8646067e07: Fixed `SingleHostDiscovery` so that it properly handles single-string `backend.listen` configurations such as `:80`. +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/config-loader@1.1.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/cli-common@0.1.10 + - @backstage/errors@1.1.4-next.1 + +## @backstage/backend-tasks@0.4.0-next.1 + +### Minor Changes + +- de8a975911: Changed to use native `AbortController` and `AbortSignal` from Node.js, instead + of the one from `node-abort-controller`. This is possible now that the minimum + supported Node.js version of the project is 16. + + Note that their interfaces are very slightly different, but typically not in a + way that matters to consumers. If you see any typescript errors as a direct + result from this, they are compatible with each other in the ways that we + interact with them, and should be possible to type-cast across without ill + effects. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/repo-tools@0.1.0-next.1 + +### Minor Changes + +- 03843259b4: Api reference documentation improvements + + - breadcrumbs links semantics as code spans + - new `@config` annotation to describe related config keys + +### Patch Changes + +- 71f80eb354: add the command type-deps to the repo tool package. +- Updated dependencies + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-catalog-backend@1.6.0-next.1 + +### Minor Changes + +- c395abb5b2: The catalog no longer stops after the first processor `validateEntityKind` + method returns `true` when validating entity kind shapes. Instead, it continues + through all registered processors that have this method, and requires that _at + least one_ of them returned true. + + The old behavior of stopping early made it harder to extend existing core kinds + with additional fields, since the `BuiltinKindsEntityProcessor` is always + present at the top of the processing chain and ensures that your additional + validation code would never be run. + + This is technically a breaking change, although it should not affect anybody + under normal circumstances, except if you had problematic validation code that + you were unaware that it was not being run. That code may now start to exhibit + those problems. + + If you need to disable this new behavior, `CatalogBuilder` as used in your + `packages/backend/src/plugins/catalog.ts` file now has a + `useLegacySingleProcessorValidation()` method to go back to the old behavior. + + ```diff + const builder = await CatalogBuilder.create(env); + +builder.useLegacySingleProcessorValidation(); + ``` + +### Patch Changes + +- 2a8e3cc0b5: Optimize `Stitcher` process to be more memory efficient +- 5b3e2afa45: Fixed deprecated use of `substr` into `substring`. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/plugin-permission-node@0.7.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-scaffolder-common@1.2.3-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.1.0-next.0 + +### Minor Changes + +- 98c643a1a2: Introduces incremental entity providers, which are used for streaming very large data sources into the catalog. + +### Patch Changes + +- de8a975911: Changed to use native `AbortController` and `AbortSignal` from Node.js, instead + of the one from `node-abort-controller`. This is possible now that the minimum + supported Node.js version of the project is 16. + + Note that their interfaces are very slightly different, but typically not in a + way that matters to consumers. If you see any typescript errors as a direct + result from this, they are compatible with each other in the ways that we + interact with them, and should be possible to type-cast across without ill + effects. + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/backend-test-utils@0.1.31-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/app-defaults@1.0.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/theme@0.2.16 + +## @backstage/backend-app-api@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-permission-node@0.7.2-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/backend-defaults@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-app-api@0.2.4-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + +## @backstage/backend-plugin-api@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/backend-test-utils@0.1.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/cli@0.21.2-next.1 + - @backstage/backend-app-api@0.2.4-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + +## @backstage/catalog-client@1.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/catalog-model@1.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/cli@0.21.2-next.1 + +### Patch Changes + +- 8fffe42708: JSX and React Fast Refresh transforms are no longer enabled when bundling backend code. +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/config-loader@1.1.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/release-manifests@0.0.8-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/errors@1.1.4-next.1 + +## @backstage/config@1.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + +## @backstage/config-loader@1.1.7-next.1 + +### Patch Changes + +- 40e7e6e1a2: Updated dependency `typescript-json-schema` to `^0.55.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/cli-common@0.1.10 + - @backstage/errors@1.1.4-next.1 + +## @backstage/core-app-api@1.2.1-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + +## @backstage/core-components@0.12.1-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/core-plugin-api@1.1.1-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + +## @backstage/create-app@0.4.35-next.1 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.10 + +## @backstage/dev-utils@1.0.9-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/test-utils@1.2.3-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/app-defaults@1.0.9-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/errors@1.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + +## @backstage/integration@1.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/integration-react@1.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/theme@0.2.16 + +## @techdocs/cli@1.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-techdocs-node@1.4.3-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/cli-common@0.1.10 + +## @backstage/test-utils@1.2.3-next.1 + +### Patch Changes + +- 5e238ed56a: The test utility for the plugin context called `MockPluginProvider` has been created. It will be handy in the cases when you use + `__experimentalConfigure` in your plugin. It is experimental and exported through `@backstage/test-utils/alpha`. +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/types@1.0.2-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. + +## @backstage/version-bridge@1.0.3-next.0 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals + +## @backstage/plugin-adr@0.2.4-next.1 + +### Patch Changes + +- a19cffbeed: Update search links to only have header as linkable text +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.4-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-adr-backend@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-adr-common@0.2.4-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-adr-common@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-airbrake@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/test-utils@1.2.3-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/dev-utils@1.0.9-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-airbrake-backend@0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + +## @backstage/plugin-allure@0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-analytics-module-ga@0.1.23-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apache-airflow@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + +## @backstage/plugin-api-docs@0.8.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-catalog@1.6.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apollo-explorer@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-app-backend@0.3.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config-loader@1.1.7-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + +## @backstage/plugin-auth-backend@0.17.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-auth-node@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-azure-devops@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-devops-backend@0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-azure-sites-backend@0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-badges-backend@0.1.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-bazaar@0.2.1-next.1 + +### Patch Changes + +- 9b1891061c: `HomePageBazaarInfoCard` is now displaying `title` instead of `name`. Title is a string that doesn't have to be URL friendly. + The BazaarOverviewCard have the new property `fullHeight`. Link in `BazaarOverviewCard`is moved to header in card. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-catalog@1.6.2-next.1 + - @backstage/cli@0.21.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-bazaar-backend@0.2.2-next.1 + +### Patch Changes + +- 9b1891061c: Column `title` has replaced column `name` for `BazaarProject` in database +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-test-utils@0.1.31-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-bitbucket-cloud-common@0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.4.1-next.1 + +## @backstage/plugin-bitrise@0.1.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-catalog@1.6.2-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- ca04d97b09: Handle refresh entity error in `AboutCard`. +- a19cffbeed: Update search links to only have header as linkable text +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-catalog-backend-module-aws@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-catalog-backend-module-azure@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.2-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.6-next.1 + +### Patch Changes + +- 4eb0bce299: Fix repo:push topic not matching `BitbucketCloudEventRouter`. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.2-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-catalog-backend-module-github@0.2.2-next.1 + +### Patch Changes + +- fe93cce743: Added the githubCredentialsProvider property to the GithubLocationAnalyzerOptions to be able to override the GithubCredentialsProvider. + +- a0fd4af94a: Handle GitHub `push` events at the `GithubEntityProvider` by subscribing to the topic `github.push.` + + Implements `EventSubscriber` to receive events for the topic `github.push`. + + On `github.push`, the affected repository will be refreshed. + This includes adding new Location entities, refreshing existing ones, + and removing obsolete ones. + + Please find more information at + + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.5-next.1 + +### Patch Changes + +- dd995cdc72: Enabled support of resolving `$refs` in all kind of yaml documents, not only OpenAPI. This implicitly adds `$ref` resolving support for AsyncAPI specs. Thus, the `openApiPlaceholderResolver` has been renamed to `jsonSchemaRefPlaceholderResolver`. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## @backstage/plugin-catalog-common@1.0.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-catalog-graph@0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-catalog-graphql@0.3.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## @backstage/plugin-catalog-import@0.9.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + +## @backstage/plugin-catalog-node@1.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + +## @backstage/plugin-catalog-react@1.2.2-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/plugin-cicd-statistics@0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/plugin-cicd-statistics@0.1.14-next.1 + +## @backstage/plugin-circleci@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-cloudbuild@0.3.12-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-climate@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-coverage@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-code-coverage-backend@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-codescene@0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-config-schema@0.1.35-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-cost-insights@0.12.1-next.1 + +### Patch Changes + +- d379b6f070: Internal refactor to avoid usage of deprecated symbols +- f97c7fd1f7: added an optional config entry `costInsights.engineerThreshold` to allow users to control the threshold value for the 'negligible' change in costs. +- a5ecdb99f0: Making a possibility to hide a trending line in a cost insights plugin +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/test-utils@1.2.3-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-dynatrace@1.0.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-events-backend@0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## @backstage/plugin-events-backend-module-aws-sqs@0.1.1-next.1 + +### Patch Changes + +- dd008a10c1: Upgrade to AWS SDK for Javascript v3 +- Updated dependencies + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## @backstage/plugin-events-backend-module-azure@0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## @backstage/plugin-events-backend-module-gerrit@0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## @backstage/plugin-events-backend-module-github@0.1.1-next.1 + +### Patch Changes + +- 0f46ec304c: Add `createGithubSignatureValidator(config)` which can be used + to create a validator used at an ingress for topic `github`. + + On top, there is a new `githubWebhookEventsModule` for the new backend plugin API + which auto-registers the `HttpPostIngress` for topic `github` incl. the validator. + + Please find more information at + . + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## @backstage/plugin-events-backend-module-gitlab@0.1.1-next.1 + +### Patch Changes + +- 31fe8f256a: Add `createGitlabTokenValidator(config)` which can be used + to create a validator used at an ingress for topic `gitlab`. + + On top, there is a new `gitlabWebhookEventsModule` for the new backend plugin API + which auto-registers the `HttpPostIngress` for topic `gitlab` incl. the validator. + + Please find more information at + . + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## @backstage/plugin-events-backend-test-utils@0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.1 + +## @backstage/plugin-events-node@0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + +## @backstage/plugin-explore@0.3.43-next.1 + +### Patch Changes + +- 4dec6f16be: Added new `@backstage/plugin-explore-backend` & `@backstage/plugin-explore-common` packages. + + This deprecates the `ExploreToolsConfig` API (in `@backstage/plugin-explore-react`) which is replaced by the `ExploreApi` & `ExploreClient`. The list of `ExploreTool` data can now be provided on the backend by either using the supplied `StaticExploreToolProvider` or by implementing a custom `ExploreToolProvider`. See the [explore-backend README](https://github.com/backstage/backstage/blob/master/plugins/explore-backend/README.md) for full details. + + NOTE: Existing installations that have customized the `ExploreToolConfig` will continue to work through the new `ExploreClient`. However, existing data should be migrated over to new `explore-backend` plugin as `ExploreToolConfig` will be removed in the future. + + BREAKING CHANGE: If you have previously installed the `explore` plugin, but not yet customized the `ExploreToolConfig` API in your `packages/app`, this will result in an empty list of tools. + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/plugin-explore-common@0.0.1-next.0 + - @backstage/plugin-explore-react@0.0.24-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-explore-backend@0.0.1-next.0 + +### Patch Changes + +- 4dec6f16be: Added new `@backstage/plugin-explore-backend` & `@backstage/plugin-explore-common` packages. + + This deprecates the `ExploreToolsConfig` API (in `@backstage/plugin-explore-react`) which is replaced by the `ExploreApi` & `ExploreClient`. The list of `ExploreTool` data can now be provided on the backend by either using the supplied `StaticExploreToolProvider` or by implementing a custom `ExploreToolProvider`. See the [explore-backend README](https://github.com/backstage/backstage/blob/master/plugins/explore-backend/README.md) for full details. + + NOTE: Existing installations that have customized the `ExploreToolConfig` will continue to work through the new `ExploreClient`. However, existing data should be migrated over to new `explore-backend` plugin as `ExploreToolConfig` will be removed in the future. + + BREAKING CHANGE: If you have previously installed the `explore` plugin, but not yet customized the `ExploreToolConfig` API in your `packages/app`, this will result in an empty list of tools. + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-explore-common@0.0.1-next.0 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-explore-common@0.0.1-next.0 + +### Patch Changes + +- 4dec6f16be: Added new `@backstage/plugin-explore-backend` & `@backstage/plugin-explore-common` packages. + + This deprecates the `ExploreToolsConfig` API (in `@backstage/plugin-explore-react`) which is replaced by the `ExploreApi` & `ExploreClient`. The list of `ExploreTool` data can now be provided on the backend by either using the supplied `StaticExploreToolProvider` or by implementing a custom `ExploreToolProvider`. See the [explore-backend README](https://github.com/backstage/backstage/blob/master/plugins/explore-backend/README.md) for full details. + + NOTE: Existing installations that have customized the `ExploreToolConfig` will continue to work through the new `ExploreClient`. However, existing data should be migrated over to new `explore-backend` plugin as `ExploreToolConfig` will be removed in the future. + + BREAKING CHANGE: If you have previously installed the `explore` plugin, but not yet customized the `ExploreToolConfig` API in your `packages/app`, this will result in an empty list of tools. + +## @backstage/plugin-explore-react@0.0.24-next.1 + +### Patch Changes + +- 4dec6f16be: Added new `@backstage/plugin-explore-backend` & `@backstage/plugin-explore-common` packages. + + This deprecates the `ExploreToolsConfig` API (in `@backstage/plugin-explore-react`) which is replaced by the `ExploreApi` & `ExploreClient`. The list of `ExploreTool` data can now be provided on the backend by either using the supplied `StaticExploreToolProvider` or by implementing a custom `ExploreToolProvider`. See the [explore-backend README](https://github.com/backstage/backstage/blob/master/plugins/explore-backend/README.md) for full details. + + NOTE: Existing installations that have customized the `ExploreToolConfig` will continue to work through the new `ExploreClient`. However, existing data should be migrated over to new `explore-backend` plugin as `ExploreToolConfig` will be removed in the future. + + BREAKING CHANGE: If you have previously installed the `explore` plugin, but not yet customized the `ExploreToolConfig` API in your `packages/app`, this will result in an empty list of tools. + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-explore-common@0.0.1-next.0 + +## @backstage/plugin-firehydrant@0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-fossa@0.2.44-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcalendar@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcp-projects@0.3.31-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-git-release-manager@0.3.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-actions@0.5.12-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-deployments@0.1.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-issues@0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-pull-requests-board@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gitops-profiles@0.3.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gocd@0.1.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphiql@0.2.44-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphql-backend@0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-catalog-graphql@0.3.16-next.1 + +## @backstage/plugin-home@0.4.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-stack-overflow@0.1.8-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-ilert@0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-jenkins@0.7.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-jenkins-common@0.1.11-next.1 + +## @backstage/plugin-jenkins-backend@0.1.29-next.1 + +### Patch Changes + +- cfe2b98fbe: Remove unnecessary dependency promise-any-polyfill +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-jenkins-common@0.1.11-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/plugin-jenkins-common@0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/plugin-kafka@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-kafka-backend@0.2.32-next.1 + +### Patch Changes + +- 0b54dbaf7d: Internal tweak to adapt to `kafkajs` 2.2.3 +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-kubernetes@0.7.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-kubernetes-common@0.4.5-next.1 + +## @backstage/plugin-kubernetes-backend@0.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-test-utils@0.1.31-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-kubernetes-common@0.4.5-next.1 + +## @backstage/plugin-kubernetes-common@0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.4-next.1 + +## @backstage/plugin-lighthouse@0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic@0.3.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic-dashboard@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-org@0.6.2-next.1 + +### Patch Changes + +- 67cedfe42e: Update `UserProfileCard` and `GroupProfileCard` to not render links unless the `showLinks` prop is set. The primary component for rendering links are the `EntityLinksCard` from plugin-catalog. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-org-react@0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-pagerduty@0.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-periskop@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-periskop-backend@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + +## @backstage/plugin-permission-backend@0.5.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/plugin-permission-node@0.7.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/plugin-permission-common@0.7.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-permission-node@0.7.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/plugin-permission-react@0.4.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/plugin-playlist@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-playlist-common@0.1.3-next.1 + +## @backstage/plugin-playlist-backend@0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-test-utils@0.1.31-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/plugin-permission-node@0.7.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-playlist-common@0.1.3-next.1 + +## @backstage/plugin-playlist-common@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/plugin-proxy-backend@0.2.33-next.1 + +### Patch Changes + +- 03843259b4: Documented the `createRouter` method. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + +## @backstage/plugin-rollbar@0.4.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-rollbar-backend@0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + +## @backstage/plugin-scaffolder@1.9.0-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-scaffolder-common@1.2.3-next.1 + +## @backstage/plugin-scaffolder-backend@1.8.1-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-scaffolder-common@1.2.3-next.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-scaffolder-backend@1.8.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-scaffolder-backend@1.8.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-scaffolder-backend@1.8.1-next.1 + - @backstage/config@1.0.5-next.1 + +## @backstage/plugin-scaffolder-common@1.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## @backstage/plugin-search@1.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-search-backend@1.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-search-backend-node@1.0.5-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/plugin-permission-node@0.7.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-search-backend-module-pg@0.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-search-backend-node@1.0.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-search-backend-node@1.0.5-next.1 + +### Patch Changes + +- de8a975911: Changed to use native `AbortController` and `AbortSignal` from Node.js, instead + of the one from `node-abort-controller`. This is possible now that the minimum + supported Node.js version of the project is 16. + + Note that their interfaces are very slightly different, but typically not in a + way that matters to consumers. If you see any typescript errors as a direct + result from this, they are compatible with each other in the ways that we + interact with them, and should be possible to type-cast across without ill + effects. + +- 54c5836f7a: Use of `TestPipeline.withSubject()` is now deprecated. Instead, use the `fromCollator`, `fromDecorator`, or `fromIndexer` static methods to instantiate a test pipeline. You may also use the class' `withCollator`, `withDecorator`, and `withIndexer` instance methods to build test pipelines that consist of multiple test subjects. + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-search-common@1.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## @backstage/plugin-search-react@1.2.2-next.1 + +### Patch Changes + +- a19cffbeed: Update search links to only have header as linkable text +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-sentry@0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-shortcuts@0.3.4-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-sonarqube@0.5.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-sonarqube-backend@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-splunk-on-call@0.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-stack-overflow@0.1.8-next.1 + +### Patch Changes + +- a19cffbeed: Update search links to only have header as linkable text +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-home@0.4.28-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-stack-overflow-backend@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.21.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-tech-insights@0.3.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-tech-insights-common@0.2.9-next.1 + +## @backstage/plugin-tech-insights-backend@0.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-tech-insights-node@0.3.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-tech-insights-common@0.2.9-next.1 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-tech-insights-node@0.3.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-tech-insights-common@0.2.9-next.1 + +## @backstage/plugin-tech-insights-common@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + +## @backstage/plugin-tech-insights-node@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-tech-insights-common@0.2.9-next.1 + +## @backstage/plugin-tech-radar@0.5.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs@1.4.1-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- a19cffbeed: Update search links to only have header as linkable text +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-catalog@1.6.2-next.1 + - @backstage/plugin-techdocs@1.4.1-next.1 + - @backstage/test-utils@1.2.3-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs-backend@1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-techdocs-node@1.4.3-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs-node@1.4.3-next.1 + +### Patch Changes + +- 0a61aab172: Bump dependency @azure/identity to next minor + +- 37931c33ce: Upgrade to AWS SDK for Javascript v3 + + Techdocs support for AWS S3 now requires defining the AWS region to connect to. + If `techdocs.publisher.awsS3.region` is missing from the config, the AWS environment variable `AWS_REGION` will be used. + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## @backstage/plugin-techdocs-react@1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## @backstage/plugin-todo@0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-todo-backend@0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-user-settings@0.6.0-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-user-settings-backend@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-vault@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @backstage/plugin-vault-backend@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-test-utils@0.1.31-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## @backstage/plugin-xcmetrics@0.2.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## example-app@0.2.78-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-cloudbuild@0.3.12-next.1 + - @backstage/plugin-gcp-projects@0.3.31-next.1 + - @backstage/plugin-github-actions@0.5.12-next.1 + - @backstage/plugin-graphiql@0.2.44-next.1 + - @backstage/plugin-techdocs@1.4.1-next.1 + - @backstage/plugin-cost-insights@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/plugin-stack-overflow@0.1.8-next.1 + - @backstage/cli@0.21.2-next.1 + - @backstage/plugin-org@0.6.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/plugin-scaffolder@1.9.0-next.1 + - @backstage/plugin-shortcuts@0.3.4-next.1 + - @backstage/plugin-user-settings@0.6.0-next.1 + - @backstage/plugin-explore@0.3.43-next.1 + - @backstage/app-defaults@1.0.9-next.1 + - @backstage/plugin-apache-airflow@0.2.5-next.1 + - @backstage/plugin-api-docs@0.8.12-next.1 + - @backstage/plugin-azure-devops@0.2.3-next.1 + - @backstage/plugin-azure-sites@0.1.1-next.1 + - @backstage/plugin-badges@0.2.36-next.1 + - @backstage/plugin-catalog-graph@0.2.24-next.1 + - @backstage/plugin-catalog-import@0.9.2-next.1 + - @backstage/plugin-circleci@0.3.12-next.1 + - @backstage/plugin-code-coverage@0.2.5-next.1 + - @backstage/plugin-dynatrace@1.0.2-next.1 + - @backstage/plugin-gcalendar@0.3.8-next.1 + - @backstage/plugin-gocd@0.1.18-next.1 + - @backstage/plugin-home@0.4.28-next.1 + - @backstage/plugin-jenkins@0.7.11-next.1 + - @backstage/plugin-kafka@0.3.12-next.1 + - @backstage/plugin-kubernetes@0.7.5-next.1 + - @backstage/plugin-lighthouse@0.3.12-next.1 + - @backstage/plugin-newrelic@0.3.30-next.1 + - @backstage/plugin-pagerduty@0.5.5-next.1 + - @backstage/plugin-playlist@0.1.3-next.1 + - @backstage/plugin-rollbar@0.4.12-next.1 + - @backstage/plugin-search@1.0.5-next.1 + - @backstage/plugin-sentry@0.4.5-next.1 + - @backstage/plugin-tech-insights@0.3.4-next.1 + - @backstage/plugin-tech-radar@0.5.19-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.7-next.1 + - @backstage/plugin-todo@0.2.14-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-airbrake@0.3.12-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.5-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @internal/plugin-catalog-customized@0.0.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## example-backend@0.2.78-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/plugin-kafka-backend@0.2.32-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/plugin-search-backend-node@1.0.5-next.1 + - @backstage/plugin-jenkins-backend@0.1.29-next.1 + - @backstage/plugin-scaffolder-backend@1.8.1-next.1 + - @backstage/plugin-explore-backend@0.0.1-next.0 + - @backstage/plugin-proxy-backend@0.2.33-next.1 + - @backstage/plugin-app-backend@0.3.39-next.1 + - @backstage/plugin-auth-backend@0.17.2-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/plugin-azure-devops-backend@0.3.18-next.1 + - @backstage/plugin-azure-sites-backend@0.1.1-next.1 + - @backstage/plugin-badges-backend@0.1.33-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/plugin-code-coverage-backend@0.2.5-next.1 + - @backstage/plugin-events-backend@0.2.0-next.1 + - @backstage/plugin-graphql-backend@0.1.29-next.1 + - @backstage/plugin-kubernetes-backend@0.8.1-next.1 + - @backstage/plugin-permission-backend@0.5.14-next.1 + - @backstage/plugin-permission-node@0.7.2-next.1 + - @backstage/plugin-playlist-backend@0.2.2-next.1 + - @backstage/plugin-rollbar-backend@0.1.36-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.7-next.1 + - @backstage/plugin-search-backend@1.1.2-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.0.5-next.1 + - @backstage/plugin-search-backend-module-pg@0.4.3-next.1 + - @backstage/plugin-tech-insights-backend@0.5.5-next.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.23-next.1 + - @backstage/plugin-tech-insights-node@0.3.7-next.1 + - @backstage/plugin-techdocs-backend@1.4.2-next.1 + - @backstage/plugin-todo-backend@0.1.36-next.1 + - example-app@0.2.78-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## example-backend-next@0.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/plugin-scaffolder-backend@1.8.1-next.1 + - @backstage/plugin-app-backend@0.3.39-next.1 + - @backstage/backend-defaults@0.1.4-next.1 + +## techdocs-cli-embedded-app@0.2.77-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-catalog@1.6.2-next.1 + - @backstage/plugin-techdocs@1.4.1-next.1 + - @backstage/test-utils@1.2.3-next.1 + - @backstage/cli@0.21.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/app-defaults@1.0.9-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## @internal/plugin-catalog-customized@0.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.6.2-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + +## @internal/plugin-todo-list@1.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## @internal/plugin-todo-list-backend@1.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## @internal/plugin-todo-list-common@1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.1 diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md index 8cb78bff60..ed550f1801 100644 --- a/docs/tutorials/configuring-plugin-databases.md +++ b/docs/tutorials/configuring-plugin-databases.md @@ -21,8 +21,8 @@ instance or cluster. With infrastructure defined as code or data (Terraform, AWS CloudFormation, etc.), you may have database credentials which lack permissions to create new databases or you do not have control over the database names. In these -instances, you can set the database name and connection information on a per -plugin basis as mentioned earlier. +instances, you can set the database connection configuration on a [per plugin basis] +(#connection-configuration-per-plugin). Backstage supports all of these use cases with the `DatabaseManager` provided by `@backstage/backend-common`. We will now cover how to use and configure diff --git a/docs/tutorials/package-role-migration.md b/docs/tutorials/package-role-migration.md index 0af5fdb615..db84defd91 100644 --- a/docs/tutorials/package-role-migration.md +++ b/docs/tutorials/package-role-migration.md @@ -116,7 +116,7 @@ The way to execute this step of the migration is not as well defined as the prev - In places where the entire repo is being built, use `yarn backstage-cli repo build`, which also supports the `--since` flag. The migration here is a bit more nuanced as it depends on why you are building all packages. - If you are building all packages to **verify** that you are able to build them, you most likely want `backstage-cli repo build --all`. The `--all` flag signals that bundled packages like `packages/app` and `packages/backend` should be built as well. Pair this up with a `--since` flag in CI to avoid needing to build all packages. - If you are building all packages to **publish** them, then `backstage-cli repo build` is enough, as it builds all published packages. - - If you are building all packages to **deploy** them, you likely don't want to use the `repo` command at all, simply call `yarn build` in the packages you want to deploy instead. For example, if you are deploying the backend with a docker host build, it's enough to call `yarn build` inside `packages/backend`. + - If you are building all packages to **deploy** them, you likely don't want to use the `repo` command at all, simply call `yarn build` in the packages you want to deploy instead. For example, if you are deploying the backend with a docker host build, it's enough to call `yarn build` inside `packages/backend`. In a standard `@backstage/create-app` there is also a shorthand for building the backend from the root, `yarn build:backend`. ## FAQ diff --git a/docs/tutorials/quickstart-app-plugin.md b/docs/tutorials/quickstart-app-plugin.md index d1ca61a11f..ea0a511c93 100644 --- a/docs/tutorials/quickstart-app-plugin.md +++ b/docs/tutorials/quickstart-app-plugin.md @@ -20,7 +20,7 @@ title: Adding Custom Plugin to Existing Monorepo App > functionality, extend the Sidebar to make our life easy. Finally, we add > custom code to display GitHub repository information. > -> This document assumes you have Node.js 14 active along with Yarn and Python. +> This document assumes you have Node.js 16 active along with Yarn and Python. > Please note, that at the time of this writing, the current version is > 0.1.1-alpha.21. This guide can still be used with future versions, just, > verify as you go. If you run into issues, you can compare your setup with mine diff --git a/microsite/blog/2022-03-04-backstage-upgrade-helper.md b/microsite/blog/2022-03-04-backstage-upgrade-helper.md index a189e28cd5..80b3b9c0a8 100644 --- a/microsite/blog/2022-03-04-backstage-upgrade-helper.md +++ b/microsite/blog/2022-03-04-backstage-upgrade-helper.md @@ -29,7 +29,7 @@ We started by creating a fork from React Native’s open source project, applyin The Backstage Upgrade Helper tool enables adopters to easily upgrade their Backstage app by using the power of git to create a diff between different versions. -Whenever a new version of Backstage is released, the Helper scaffolds a new dummy Backstage app using the `backstage-create-app` cli utility and checks all the generated files in a specific git branch. After the branch is generated, it gets compared with all the existing ones, which results in generating specific git patches stored in specific files. By selecting the version of the current Backstage release together with the version you want to upgrade to, the UI knows which patch file needs to be picked up. +Whenever a new version of Backstage is released, the Helper scaffolds a new sample Backstage app using the `backstage-create-app` cli utility and checks all the generated files in a specific git branch. After the branch is generated, it gets compared with all the existing ones, which results in generating specific git patches stored in specific files. By selecting the version of the current Backstage release together with the version you want to upgrade to, the UI knows which patch file needs to be picked up. So, now you can update your Backstage application in three steps rather than manually reading the changelogs of all the Backstage modules in reverse order. diff --git a/microsite/blog/2022-10-28-backstagecon-kubecon-2022.md b/microsite/blog/2022-10-28-backstagecon-kubecon-2022.md new file mode 100644 index 0000000000..ffc44ed432 --- /dev/null +++ b/microsite/blog/2022-10-28-backstagecon-kubecon-2022.md @@ -0,0 +1,36 @@ +--- +title: 'ICYMI: BackstageCon & KubeCon NA 2022 Recap' +description: 'We had a great time in Detroit! Learn more about our first dedicated conference to Backstage and KubeCon NA 2022.' +author: Emma White, Spotify +authorURL: https://www.linkedin.com/in/emmamckeewhite/ +--- + +## ICYMI: BackstageCon & KubeCon NA 2022 Recap + +![blog header image](assets/2022-10-28/preview.png) + +What an action-packed and memorable week for the Backstage community meeting up IRL at KubeCon North America 2022 in (unseasonably warm and sunny) Detroit. Let’s see what the community was up to: + +### BackstageCon: A full day of nothing but Backstage + +![BackstageCon group](assets/2022-10-28/groupimage.jpg) + +We started the week off strong, packing a ballroom for [BackstageCon](https://events.linuxfoundation.org/backstagecon-north-america/) on Monday. It was a smorgasbord of some of our active community members at the first official Backstage conference — as well as some new faces interested in learning more about Backstage! To welcome us, Roadie’s [Martina Iglesias Fernandez](https://github.com/martina-if) and Spotify’s [Suzanne Daniels](https://github.com/suuus) took center stage. Next up, we listened to three keynotes from the event’s top sponsors: Roadie’s [David Tuite](https://github.com/dtuite) enlightened us on how to 10x developer effectiveness for teams, [Dave Zolotusky](https://github.com/dzolotusky) from Spotify discussed developer happiness and [announced](https://www.linkedin.com/posts/spotify-r%26d_backstage-devex-activity-6990305201710587904-SnDQ?utm_source=share&utm_medium=member_desktop) that [Spotify Plugins for Backstage](https://backstage.spotify.com/blog/spotify-plugins-for-backstage/) are launching soon, and [Valentina Alaria](https://www.linkedin.com/in/valentinaalaria/) from VMware Tanzu talked about how to curate the Backstage experience to enable developer velocity. In between some creative snacks (lemon blueberry popcorn, anyone?) and coffee breaks, we heard from adopters including [Telus](https://backstage.spotify.com/blog/building-for-simplicity-TELUS/), [DAZN](https://backstage.spotify.com/blog/adopter-spotlight/dazn/), HP, US Bank, and more on lessons learned through their adoption journeys, we talked about different ways to leverage data within Backstage, and we discussed new ways to customize Backstage for specific adopter needs. + +You can watch all the BackstageCon sessions, talks, and keynotes [here](https://www.youtube.com/playlist?list=PLj6h78yzYM2OKySsTuiip3BqmdYZQRnSf)! + +### Project Meeting: Building a better Backstage together + +![project meeting image](assets/2022-10-28/real_project_meeting.jpeg) + +The following day, the Backstage maintainers hosted a lively project meeting, where we engaged with a lot of folks that had been at BackstageCon the day before. Over the course of the meeting, the Backstage community talked through recommended approaches for maintaining your Software Catalog as a source of truth, sourcing internal contributors for organization’s Backstage apps, improving frontend performance in a few plugins, how Backstage maintainers work, and appreciation for the contributions from our community. You can read more detailed notes from the project meeting in the repo [here](https://github.com/backstage/community/blob/main/project-meetings/kubecon-na-2022.md). + +### KubeCon: Demos, swag, and standing-room only + +And last but not least: three days of [KubeCon North America](https://events.linuxfoundation.org/kubecon-cloudnativecon-north-america/)! At KubeCon, the Backstage team met and greeted so many new faces at our kiosk in the Project Pavilion. We ran demos, answered questions, and handed out lots and lots of SWAG! + +![maintainer presentation](assets/2022-10-28/project_meeting.jpg) + +Spotify’s [Francesco Corti](https://github.com/fcorti) and core maintainer [Ben Lambert](https://github.com/benjdlambert) took the stage on Wednesday to present the Backstage maintainer talk to a PACKED audience. They discussed how core features of Backstage are changing the way end-user developers interact and manage their ecosystem, and encouraged folks to create the future of developer experience with the rest of the Backstage community. If you missed it, no worries! Their talk track will be posted on the [CNCF YouTube channel](https://www.youtube.com/c/cloudnativefdn). + +…and that’s a wrap! What an incredible week. Our growing Backstage community continues to amaze us with its dedication and enthusiasm. We can’t wait to see you all again at KubeCon Europe in The Netherlands! In fact, we’d love to see our adopters give Backstage talks next year (hint, hint). The KubeCon Europe [CFP is open](https://events.linuxfoundation.org/kubecon-cloudnativecon-europe/program/cfp/#overview) until Friday, November 18, 2022. diff --git a/microsite/blog/2022-11-15-linux-foundation-introduction-to-backstage-course.md b/microsite/blog/2022-11-15-linux-foundation-introduction-to-backstage-course.md new file mode 100644 index 0000000000..b762bd7e97 --- /dev/null +++ b/microsite/blog/2022-11-15-linux-foundation-introduction-to-backstage-course.md @@ -0,0 +1,16 @@ +--- +title: The Linux Foundation launches its Introduction to Backstage course +description: The new Linux Foundation course is available for free on edX and is designed to help individuals understand how to map Backstage to their organization needs +author: Jorge Lainfiesta, Roadie +authorURL: https://www.linkedin.com/in/jrlainfiesta/ +--- + +Backstage continues to grow in [popularity and maturity](https://roadie.io/blog/backstage-consolidating-its-role-in-the-cloud-native-ecosystem/), with industry leaders not only adopting the framework but actively participating in the community through contributions and commercial offerings. One of the most recent contributions comes from the Linux Foundation, [launching an introductory course](https://training.linuxfoundation.org/blog/23107/) aimed at DevOps engineers or professionals working on Developer Productivity or Developer Experience. The course is available for free on edX, with a paid option for the certified track. + +[![Introduction to Backstage: Developer Portals Made Easy (LFS142x): Enroll!](assets/22-11-15/Introduction-to-Backstage-Developer-Portals-Made-Easy-2-768x432.png)](https://www.edx.org/course/introduction-to-backstage-developer-portals-made-easy) + + + +The course, [Introduction to Backstage: Developer Portals Made Easy (LFS142x)](https://www.edx.org/course/introduction-to-backstage-developer-portals-made-easy), starts discussing the benefits of adopting a Developer Portal. Then, it dives into what is Backstage, its main features (Catalog, Scaffolder, TechDocs), and how to map its capabilities to your organization. At last, it provides tips on how to connect with the community to ease up your adoption journey. + +The course author, [Jorge Lainfiesta](https://www.linkedin.com/in/jrlainfiesta/), is a Technical Marketing Manager at [Roadie](https://roadie.io). Jorge has a background in software engineering (PayPal) and digital communication (UCLA). He’s been working around Backstage since it was open sourced by Spotify and co-hosts community initiatives like the Backstage Users Unconference. diff --git a/microsite/blog/assets/2022-10-28/groupimage.jpg b/microsite/blog/assets/2022-10-28/groupimage.jpg new file mode 100644 index 0000000000..ef12667e0a Binary files /dev/null and b/microsite/blog/assets/2022-10-28/groupimage.jpg differ diff --git a/microsite/blog/assets/2022-10-28/preview.png b/microsite/blog/assets/2022-10-28/preview.png new file mode 100644 index 0000000000..67774e7744 Binary files /dev/null and b/microsite/blog/assets/2022-10-28/preview.png differ diff --git a/microsite/blog/assets/2022-10-28/project_meeting.jpg b/microsite/blog/assets/2022-10-28/project_meeting.jpg new file mode 100644 index 0000000000..8e3bc91ddf Binary files /dev/null and b/microsite/blog/assets/2022-10-28/project_meeting.jpg differ diff --git a/microsite/blog/assets/2022-10-28/real_project_meeting.jpeg b/microsite/blog/assets/2022-10-28/real_project_meeting.jpeg new file mode 100644 index 0000000000..f1e0ff40bf Binary files /dev/null and b/microsite/blog/assets/2022-10-28/real_project_meeting.jpeg differ diff --git a/microsite/blog/assets/22-11-15/Introduction-to-Backstage-Developer-Portals-Made-Easy-2-768x432.png b/microsite/blog/assets/22-11-15/Introduction-to-Backstage-Developer-Portals-Made-Easy-2-768x432.png new file mode 100644 index 0000000000..a789762cc1 Binary files /dev/null and b/microsite/blog/assets/22-11-15/Introduction-to-Backstage-Developer-Portals-Made-Easy-2-768x432.png differ diff --git a/microsite/data/plugins/announcements.yaml b/microsite/data/plugins/announcements.yaml new file mode 100644 index 0000000000..de80dcf6fe --- /dev/null +++ b/microsite/data/plugins/announcements.yaml @@ -0,0 +1,10 @@ +--- +title: Announcements +author: K-Phoen +authorUrl: https://github.com/K-Phoen +category: Discovery +description: Write and share announcements within Backstage. +documentation: https://github.com/K-Phoen/backstage-plugin-announcements/ +iconUrl: img/plugin-announcements-logo.png +npmPackageName: '@k-phoen/backstage-plugin-announcements' +addedDate: '2022-11-09' diff --git a/microsite/data/plugins/azure-spring-apps.yml b/microsite/data/plugins/azure-spring-apps.yml new file mode 100644 index 0000000000..5b41885ac4 --- /dev/null +++ b/microsite/data/plugins/azure-spring-apps.yml @@ -0,0 +1,10 @@ +--- +title: 'Azure Spring Apps' +author: Enfuse +authorUrl: https://enfuse.io/ +category: Discovery +description: Easily view your Azure Spring Apps service resources +documentation: https://github.com/enfuse/asae-backstage-plugin/blob/main/README.md +iconUrl: img/enfuse.png +npmPackageName: '@enfuse/plugin-azure-spring-apps' +addedDate: '2022-11-21' diff --git a/microsite/data/plugins/cicd-statistics.yaml b/microsite/data/plugins/cicd-statistics.yaml new file mode 100644 index 0000000000..a43b56b0d9 --- /dev/null +++ b/microsite/data/plugins/cicd-statistics.yaml @@ -0,0 +1,13 @@ +--- +title: CI/CD Statistics +author: Spotify +authorUrl: https://github.com/spotify +category: CI/CD +description: Visualize CI/CD pipeline statistics such as build time or success and error rates. +documentation: https://github.com/backstage/backstage/tree/master/plugins/cicd-statistics +iconUrl: img/cicd-statistics.svg +npmPackageName: '@backstage/plugin-cicd-statistics' +tags: + - ci + - cd +addedDate: '2022-01-13' diff --git a/microsite/data/plugins/cloudsmith.yaml b/microsite/data/plugins/cloudsmith.yaml new file mode 100644 index 0000000000..5fc417eec1 --- /dev/null +++ b/microsite/data/plugins/cloudsmith.yaml @@ -0,0 +1,13 @@ +--- +title: Cloudsmith +author: Roadie +authorUrl: https://roadie.io +category: CI/CD +description: Show Cloudsmith Repository stats on your backstage homepage. +documentation: https://github.com/RoadieHQ/roadie-backstage-plugins/tree/main/plugins/frontend/backstage-plugin-cloudsmith +iconUrl: https://cloudsmith.com/img/cloudsmith-mini-dark.svg +npmPackageName: '@roadiehq/backstage-plugin-cloudsmith' +tags: + - dashboards + - monitoring +addedDate: '2022-11-18' diff --git a/microsite/data/plugins/humanitec.yaml b/microsite/data/plugins/humanitec.yaml index 5b3c459116..1b79270582 100644 --- a/microsite/data/plugins/humanitec.yaml +++ b/microsite/data/plugins/humanitec.yaml @@ -6,7 +6,7 @@ category: Deployment # A single category e.g. CI, Machine Learning, Services, Mo description: | Show workloads, environments and resources deployed by Humanitec Platform Orchestrator. Plugin includes an Entity ComponentCard, Backend API route and scaffolder actions. -documentation: https://github.com/thefrontside/backstage/tree/main/plugins/humanitec +documentation: https://github.com/thefrontside/playhouse/tree/main/plugins/humanitec iconUrl: img/humanitec-logo.png npmPackageName: '@frontside/backstage-plugin-humanitec' addedDate: '2022-06-22' diff --git a/microsite/pages/en/community.js b/microsite/pages/en/community.js index 2e7801d581..a5fd5353e0 100644 --- a/microsite/pages/en/community.js +++ b/microsite/pages/en/community.js @@ -103,64 +103,79 @@ const Background = props => { Stay tuned to the latest developments + - - - Community sessions - - Maintainers and adopters meet monthly to share updates, demos, - and ideas. Yep, all sessions are recorded! - + + + + Community sessions + + Maintainers and adopters meet monthly to share updates, demos, + and ideas. Yep, all sessions are recorded! + + Join a session - - - Newsletter - - The official monthly Backstage newsletter. Don't miss the latest - news from your favorite project! - + + + + + Newsletter + + The official monthly Backstage newsletter. Don't miss the + latest news from your favorite project! + + Subscribe - - - Contributor Spotlight - - A recognition for valuable community work. Nominate contributing - members for their efforts! We'll put them in the spotlight ❤️ - + + + + + Contributor Spotlight + + A recognition for valuable community work. Nominate + contributing members for their efforts! We'll put them in the + spotlight ❤️ + + Nominate now - + + Community initiatives - - Open Mic Meetup - - A casual get together of Backstage users sharing their experiences - and helping each other. Hosted by{' '} - Roadie.io and{' '} - Frontside Software. - + + + Open Mic Meetup + + A casual get together of Backstage users sharing their + experiences and helping each other. Hosted by{' '} + Roadie.io and{' '} + Frontside Software. + + Learn more - - Backstage Weekly Newsletter - - A weekly newsletter with news, updates and things community from - your friends at Roadie.io. - + + + Backstage Weekly Newsletter + + A weekly newsletter with news, updates and things community from + your friends at Roadie.io. + + Learn more @@ -168,6 +183,29 @@ const Background = props => { + + + + Trainings and Certifications + + + + + Introduction to Backstage: Developer Portals Made Easy (LFS142x) + + + This is a course produced and curated by the Linux Foundation. + This course introduces you to Backstage and how to get started + with the project. + + + + Learn more + + + + + diff --git a/microsite/pages/en/index.js b/microsite/pages/en/index.js index 8e963c2453..ab3dabe14e 100644 --- a/microsite/pages/en/index.js +++ b/microsite/pages/en/index.js @@ -31,15 +31,6 @@ class Index extends React.Component { ! - -
- 🤩 Join us for the first{' '} - - BackstageCon - {' '} - on October 24, 2022! -
-
diff --git a/microsite/pages/en/live.js b/microsite/pages/en/live.js index 2accd6d11c..11945fcd3b 100644 --- a/microsite/pages/en/live.js +++ b/microsite/pages/en/live.js @@ -34,7 +34,7 @@ const Background = props => { diff --git a/microsite/sidebars.json b/microsite/sidebars.json index e758fe9100..581f8b592d 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -1,6 +1,7 @@ { "releases": { "Release Notes": [ + "releases/v1.8.0", "releases/v1.7.0", "releases/v1.6.0", "releases/v1.5.0", @@ -95,6 +96,7 @@ "features/software-templates/configuration", "features/software-templates/adding-templates", "features/software-templates/writing-templates", + "features/software-templates/input-examples", "features/software-templates/builtin-actions", "features/software-templates/writing-custom-actions", "features/software-templates/writing-custom-field-extensions", @@ -315,6 +317,7 @@ ], "Designing for Backstage": [ "dls/design", + "dls/component-design-guidelines", "dls/contributing-to-storybook", "dls/figma" ], diff --git a/microsite/static/img/cicd-statistics.svg b/microsite/static/img/cicd-statistics.svg new file mode 100644 index 0000000000..302f5e8f98 --- /dev/null +++ b/microsite/static/img/cicd-statistics.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/microsite/static/img/enfuse.png b/microsite/static/img/enfuse.png new file mode 100644 index 0000000000..42124a4a24 Binary files /dev/null and b/microsite/static/img/enfuse.png differ diff --git a/microsite/static/img/plugin-announcements-logo.png b/microsite/static/img/plugin-announcements-logo.png new file mode 100644 index 0000000000..edb0caf8d9 Binary files /dev/null and b/microsite/static/img/plugin-announcements-logo.png differ diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 0a943bbe83..0a26cf8ef6 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -281,14 +281,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.10.4, @babel/helper-validator-identifier@npm:^7.12.11": - version: 7.12.11 - resolution: "@babel/helper-validator-identifier@npm:7.12.11" - checksum: e604c6bf890704fc46c1ae13bf23afb242b810224ec3403bba67cdbf0d8dabfec4b82123d6dfb18135a0ee3f7f79218583c819363ebb5e04a0a49d8418db7fce - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.18.6": +"@babel/helper-validator-identifier@npm:^7.10.4, @babel/helper-validator-identifier@npm:^7.18.6": version: 7.18.6 resolution: "@babel/helper-validator-identifier@npm:7.18.6" checksum: e295254d616bbe26e48c196a198476ab4d42a73b90478c9842536cf910ead887f5af6b5c4df544d3052a25ccb3614866fa808dc1e3a5a4291acd444e243c0648 @@ -1241,18 +1234,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.10.4, @babel/types@npm:^7.10.5, @babel/types@npm:^7.12.1, @babel/types@npm:^7.12.10, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.12, @babel/types@npm:^7.12.5, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.4.4": - version: 7.12.12 - resolution: "@babel/types@npm:7.12.12" - dependencies: - "@babel/helper-validator-identifier": ^7.12.11 - lodash: ^4.17.19 - to-fast-properties: ^2.0.0 - checksum: 1eefe9468573a32491030f2986f820eb4289ff0b93e437b97f665e73cc8dae0ec0bdd95f37995bb3b70b5ce2a9a45a302affbbad497150021cf874c5ef10e361 - languageName: node - linkType: hard - -"@babel/types@npm:^7.8.3": +"@babel/types@npm:^7.10.4, @babel/types@npm:^7.10.5, @babel/types@npm:^7.12.1, @babel/types@npm:^7.12.10, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.12, @babel/types@npm:^7.12.5, @babel/types@npm:^7.12.6, @babel/types@npm:^7.12.7, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.18.10 resolution: "@babel/types@npm:7.18.10" dependencies: @@ -2917,7 +2899,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.3.3": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.3": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -2929,7 +2911,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4.3.1, debug@npm:^4.1.0": +"debug@npm:4.3.1": version: 4.3.1 resolution: "debug@npm:4.3.1" dependencies: @@ -2958,9 +2940,9 @@ __metadata: linkType: hard "decode-uri-component@npm:^0.2.0": - version: 0.2.0 - resolution: "decode-uri-component@npm:0.2.0" - checksum: f3749344ab9305ffcfe4bfe300e2dbb61fc6359e2b736812100a3b1b6db0a5668cba31a05e4b45d4d63dbf1a18dfa354cd3ca5bb3ededddabb8cd293f4404f94 + version: 0.2.2 + resolution: "decode-uri-component@npm:0.2.2" + checksum: 95476a7d28f267292ce745eac3524a9079058bbb35767b76e3ee87d42e34cd0275d2eb19d9d08c3e167f97556e8a2872747f5e65cbebcac8b0c98d83e285f139 languageName: node linkType: hard @@ -4275,21 +4257,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.0.5, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.6": - version: 7.2.0 - resolution: "glob@npm:7.2.0" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.0.4 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 78a8ea942331f08ed2e055cb5b9e40fe6f46f579d7fd3d694f3412fe5db23223d29b7fee1575440202e9a7ff9a72ab106a39fee39934c7bedafe5e5f8ae20134 - languageName: node - linkType: hard - -"glob@npm:^7.1.4": +"glob@npm:^7.0.0, glob@npm:^7.0.5, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -4430,14 +4398,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0": - version: 4.2.4 - resolution: "graceful-fs@npm:4.2.4" - checksum: 9d58c444eb4f391ce30b451aae8a8af2bd675d9f6f624719e97306f571ab89b2bd2b5f9025199bc63a2edfe2e53e7701554012f32a708148d53aa689163728cc - languageName: node - linkType: hard - -"graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": version: 4.2.10 resolution: "graceful-fs@npm:4.2.10" checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da @@ -5056,15 +5017,6 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.1.0": - version: 2.2.0 - resolution: "is-core-module@npm:2.2.0" - dependencies: - has: ^1.0.3 - checksum: 61e2aff4a7db4f8f7d5a97b484808af17290f4197b34a797cd3d3d27b6b448951064f8d3d6ceae4394fa9b7e6cf08aacd2ba7a17ef6352e922fe803580fbde56 - languageName: node - linkType: hard - "is-core-module@npm:^2.8.0": version: 2.8.1 resolution: "is-core-module@npm:2.8.1" @@ -6196,7 +6148,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:3.0.4, minimatch@npm:^3.0.4, minimatch@npm:~3.0.2": +"minimatch@npm:3.0.4, minimatch@npm:~3.0.2": version: 3.0.4 resolution: "minimatch@npm:3.0.4" dependencies: @@ -6205,7 +6157,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.1.1": +"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -6941,7 +6893,7 @@ __metadata: languageName: node linkType: hard -"path-parse@npm:^1.0.6, path-parse@npm:^1.0.7": +"path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a @@ -7441,11 +7393,11 @@ __metadata: linkType: hard "prettier@npm:^2.6.2": - version: 2.7.1 - resolution: "prettier@npm:2.7.1" + version: 2.8.0 + resolution: "prettier@npm:2.8.0" bin: prettier: bin-prettier.js - checksum: 55a4409182260866ab31284d929b3cb961e5fdb91fe0d2e099dac92eaecec890f36e524b4c19e6ceae839c99c6d7195817579cdffc8e2c80da0cb794463a748b + checksum: 72004ce0cc9bb097daf3e3833f62495768724392c1d5b178dd47372337616e9e50ecbb0804f236596223f7b5eb1bbe69cefc8957dca21112c5777e77ef73a564 languageName: node linkType: hard @@ -7967,7 +7919,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.6": +"resolve@npm:^1.1.6, resolve@npm:^1.10.0": version: 1.21.0 resolution: "resolve@npm:1.21.0" dependencies: @@ -7980,17 +7932,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.10.0": - version: 1.19.0 - resolution: "resolve@npm:1.19.0" - dependencies: - is-core-module: ^2.1.0 - path-parse: ^1.0.6 - checksum: a05b356e47b85ad3613d9e2a39a824f3c27f4fcad9c9ff6c7cc71a2e314c5904a90ab37481ad0069d03cab9eaaac6eb68aca1bc3355fdb05f1045cd50e2aacea - languageName: node - linkType: hard - -"resolve@patch:resolve@^1.1.6#~builtin": +"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.10.0#~builtin": version: 1.21.0 resolution: "resolve@patch:resolve@npm%3A1.21.0#~builtin::version=1.21.0&hash=07638b" dependencies: @@ -8003,16 +7945,6 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.10.0#~builtin": - version: 1.19.0 - resolution: "resolve@patch:resolve@npm%3A1.19.0#~builtin::version=1.19.0&hash=07638b" - dependencies: - is-core-module: ^2.1.0 - path-parse: ^1.0.6 - checksum: 2443b94d347e6946c87c85faf13071f605e609e0b54784829b0ed2b917d050bfc1cbaf4ecc6453f224cfa7d0c5dcd97cbb273454cd210bee68e4af15c1a5abc9 - languageName: node - linkType: hard - "responselike@npm:1.0.2": version: 1.0.2 resolution: "responselike@npm:1.0.2" @@ -8313,14 +8245,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2": - version: 3.0.3 - resolution: "signal-exit@npm:3.0.3" - checksum: f0169d3f1263d06df32ca072b0bf33b34c6f8f0341a7a1621558a2444dfbe8f5fec76b35537fcc6f0bc4944bdb5336fe0bdcf41a5422c4e45a1dba3f45475e6c - languageName: node - linkType: hard - -"signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 diff --git a/mkdocs.yml b/mkdocs.yml index 43efc32f8e..f3194f0d61 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,6 +62,7 @@ nav: - Configuration: 'features/software-templates/configuration.md' - Adding your own Templates: 'features/software-templates/adding-templates.md' - Writing Templates: 'features/software-templates/writing-templates.md' + - Input Examples: 'features/software-templates/input-examples.md' - Builtin Actions: 'features/software-templates/builtin-actions.md' - Writing Custom Actions: 'features/software-templates/writing-custom-actions.md' - Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md' @@ -177,6 +178,7 @@ nav: - Heroku: 'deployment/heroku.md' - Designing for Backstage: - Design: 'dls/design.md' + - Component Design Guidelines: 'dls/component-design-guidelines.md' - Contributing to Storybook: 'dls/contributing-to-storybook.md' - Figma: 'dls/figma.md' - API Reference: diff --git a/package.json b/package.json index 168f24787d..524cdf4bbe 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,16 @@ "name": "root", "private": true, "engines": { - "node": "14 || 16" + "node": "16 || 18" }, "scripts": { "dev": "concurrently \"yarn start\" \"yarn start-backend\"", "start": "yarn workspace example-app start", "start-backend": "yarn workspace example-backend start", - "build": "backstage-cli repo build --all", + "build:backend": "yarn workspace backend build", + "build:all": "backstage-cli repo build --all", "build:api-reports": "yarn build:api-reports:only --tsc", - "build:api-reports:only": "ts-node -T -P scripts/tsconfig.json scripts/api-extractor.ts", + "build:api-reports:only": "backstage-repo-tools api-reports", "build:api-docs": "LANG=en_EN yarn build:api-reports --docs", "tsc": "tsc", "tsc:full": "backstage-cli repo clean && tsc --skipLibCheck false --incremental false", @@ -20,7 +21,7 @@ "lint": "backstage-cli repo lint --since origin/master", "lint:docs": "node ./scripts/check-docs-quality", "lint:all": "backstage-cli repo lint", - "lint:type-deps": "node scripts/check-type-dependencies.js", + "lint:type-deps": "backstage-repo-tools type-deps", "docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image", "new": "backstage-cli new --scope backstage --baseVersion 0.0.0 --no-private", "create-plugin": "echo \"use 'yarn new' instead\"", @@ -32,7 +33,8 @@ "build-storybook": "yarn ./storybook run build-storybook", "techdocs-cli": "node scripts/techdocs-cli.js", "techdocs-cli:dev": "cross-env TECHDOCS_CLI_DEV_MODE=true node scripts/techdocs-cli.js", - "prepare": "husky install" + "prepare": "husky install", + "postinstall": "husky install" }, "workspaces": { "packages": [ @@ -44,19 +46,16 @@ "@types/react": "^17", "@types/react-dom": "^17" }, - "version": "1.8.0-next.0", + "version": "1.9.0-next.1", "dependencies": { "@backstage/errors": "workspace:^", - "@manypkg/get-packages": "^1.1.3", - "@microsoft/api-documenter": "^7.17.11", - "@microsoft/api-extractor": "^7.23.0", - "@microsoft/api-extractor-model": "^7.17.2", - "@microsoft/tsdoc": "^0.14.1" + "@manypkg/get-packages": "^1.1.3" }, "devDependencies": { "@backstage/cli": "workspace:*", "@backstage/codemods": "workspace:*", "@backstage/create-app": "workspace:*", + "@backstage/repo-tools": "workspace:*", "@changesets/cli": "^2.14.0", "@octokit/rest": "^19.0.3", "@spotify/prettier-config": "^14.0.0", @@ -69,10 +68,12 @@ "e2e-test": "workspace:*", "eslint": "^8.6.0", "eslint-plugin-notice": "^0.9.10", + "eslint-plugin-testing-library": "^5.9.1", "fs-extra": "10.1.0", "husky": "^8.0.0", "lint-staged": "^13.0.0", "minimist": "^1.2.5", + "node-gyp": "^9.1.0", "prettier": "^2.2.1", "semver": "^7.3.2", "shx": "^0.3.2", @@ -90,6 +91,12 @@ ], "*.md": [ "node ./scripts/check-docs-quality" + ], + "./yarn.lock": [ + "node ./scripts/verify-lockfile-duplicates --fix" + ], + "*/yarn.lock": [ + "node ./scripts/verify-lockfile-duplicates --fix" ] }, "packageManager": "yarn@3.2.3" diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 12b6a0894f..7485f130e3 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/app-defaults +## 1.0.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/theme@0.2.16 + +## 1.0.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.8-next.0 + +## 1.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-app-api@1.2.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.7 + +## 1.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.7-next.0 + ## 1.0.8-next.0 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 6658f0a8bd..e7acda4732 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "1.0.8-next.0", + "version": "1.0.9-next.1", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index d909773ff4..813a669173 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,266 @@ # example-app +## 0.2.78-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-cloudbuild@0.3.12-next.1 + - @backstage/plugin-gcp-projects@0.3.31-next.1 + - @backstage/plugin-github-actions@0.5.12-next.1 + - @backstage/plugin-graphiql@0.2.44-next.1 + - @backstage/plugin-techdocs@1.4.1-next.1 + - @backstage/plugin-cost-insights@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/plugin-stack-overflow@0.1.8-next.1 + - @backstage/cli@0.21.2-next.1 + - @backstage/plugin-org@0.6.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/plugin-scaffolder@1.9.0-next.1 + - @backstage/plugin-shortcuts@0.3.4-next.1 + - @backstage/plugin-user-settings@0.6.0-next.1 + - @backstage/plugin-explore@0.3.43-next.1 + - @backstage/app-defaults@1.0.9-next.1 + - @backstage/plugin-apache-airflow@0.2.5-next.1 + - @backstage/plugin-api-docs@0.8.12-next.1 + - @backstage/plugin-azure-devops@0.2.3-next.1 + - @backstage/plugin-azure-sites@0.1.1-next.1 + - @backstage/plugin-badges@0.2.36-next.1 + - @backstage/plugin-catalog-graph@0.2.24-next.1 + - @backstage/plugin-catalog-import@0.9.2-next.1 + - @backstage/plugin-circleci@0.3.12-next.1 + - @backstage/plugin-code-coverage@0.2.5-next.1 + - @backstage/plugin-dynatrace@1.0.2-next.1 + - @backstage/plugin-gcalendar@0.3.8-next.1 + - @backstage/plugin-gocd@0.1.18-next.1 + - @backstage/plugin-home@0.4.28-next.1 + - @backstage/plugin-jenkins@0.7.11-next.1 + - @backstage/plugin-kafka@0.3.12-next.1 + - @backstage/plugin-kubernetes@0.7.5-next.1 + - @backstage/plugin-lighthouse@0.3.12-next.1 + - @backstage/plugin-newrelic@0.3.30-next.1 + - @backstage/plugin-pagerduty@0.5.5-next.1 + - @backstage/plugin-playlist@0.1.3-next.1 + - @backstage/plugin-rollbar@0.4.12-next.1 + - @backstage/plugin-search@1.0.5-next.1 + - @backstage/plugin-sentry@0.4.5-next.1 + - @backstage/plugin-tech-insights@0.3.4-next.1 + - @backstage/plugin-tech-radar@0.5.19-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.7-next.1 + - @backstage/plugin-todo@0.2.14-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-airbrake@0.3.12-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.5-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @internal/plugin-catalog-customized@0.0.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 0.2.78-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-graph@0.2.24-next.0 + - @backstage/plugin-pagerduty@0.5.5-next.0 + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/plugin-scaffolder@1.9.0-next.0 + - @backstage/cli@0.21.2-next.0 + - @backstage/plugin-cost-insights@0.12.1-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/plugin-user-settings@0.6.0-next.0 + - @backstage/plugin-explore@0.3.43-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/plugin-airbrake@0.3.12-next.0 + - @backstage/plugin-apache-airflow@0.2.5-next.0 + - @backstage/plugin-api-docs@0.8.12-next.0 + - @backstage/plugin-azure-devops@0.2.3-next.0 + - @backstage/plugin-azure-sites@0.1.1-next.0 + - @backstage/plugin-badges@0.2.36-next.0 + - @backstage/plugin-catalog-import@0.9.2-next.0 + - @backstage/plugin-circleci@0.3.12-next.0 + - @backstage/plugin-cloudbuild@0.3.12-next.0 + - @backstage/plugin-code-coverage@0.2.5-next.0 + - @backstage/plugin-dynatrace@1.0.2-next.0 + - @backstage/plugin-gcalendar@0.3.8-next.0 + - @backstage/plugin-gcp-projects@0.3.31-next.0 + - @backstage/plugin-github-actions@0.5.12-next.0 + - @backstage/plugin-gocd@0.1.18-next.0 + - @backstage/plugin-graphiql@0.2.44-next.0 + - @backstage/plugin-home@0.4.28-next.0 + - @backstage/plugin-jenkins@0.7.11-next.0 + - @backstage/plugin-kafka@0.3.12-next.0 + - @backstage/plugin-kubernetes@0.7.5-next.0 + - @backstage/plugin-lighthouse@0.3.12-next.0 + - @backstage/plugin-newrelic@0.3.30-next.0 + - @backstage/plugin-org@0.6.1-next.0 + - @backstage/plugin-playlist@0.1.3-next.0 + - @backstage/plugin-rollbar@0.4.12-next.0 + - @backstage/plugin-search@1.0.5-next.0 + - @backstage/plugin-sentry@0.4.5-next.0 + - @backstage/plugin-shortcuts@0.3.4-next.0 + - @backstage/plugin-stack-overflow@0.1.8-next.0 + - @backstage/plugin-tech-insights@0.3.4-next.0 + - @backstage/plugin-tech-radar@0.5.19-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.7-next.0 + - @backstage/plugin-techdocs@1.4.1-next.0 + - @backstage/plugin-todo@0.2.14-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/app-defaults@1.0.9-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-newrelic-dashboard@0.2.5-next.0 + - @backstage/plugin-permission-react@0.4.8-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + - @internal/plugin-catalog-customized@0.0.5-next.0 + +## 0.2.77 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.8.0 + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/plugin-techdocs@1.4.0 + - @backstage/core-components@0.12.0 + - @backstage/plugin-cost-insights@0.12.0 + - @backstage/plugin-apache-airflow@0.2.4 + - @backstage/core-app-api@1.2.0 + - @backstage/plugin-explore@0.3.42 + - @backstage/plugin-tech-insights@0.3.3 + - @backstage/plugin-github-actions@0.5.11 + - @backstage/cli@0.21.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-dynatrace@1.0.1 + - @backstage/plugin-catalog-import@0.9.1 + - @backstage/plugin-catalog-graph@0.2.23 + - @backstage/plugin-tech-radar@0.5.18 + - @backstage/plugin-playlist@0.1.2 + - @backstage/plugin-stack-overflow@0.1.7 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-gcalendar@0.3.7 + - @backstage/plugin-code-coverage@0.2.4 + - @backstage/plugin-org@0.6.0 + - @backstage/plugin-api-docs@0.8.11 + - @backstage/plugin-kubernetes@0.7.4 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.6 + - @backstage/plugin-circleci@0.3.11 + - @backstage/plugin-azure-sites@0.1.0 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/plugin-cloudbuild@0.3.11 + - @backstage/plugin-jenkins@0.7.10 + - @backstage/plugin-kafka@0.3.11 + - @backstage/plugin-pagerduty@0.5.4 + - @backstage/plugin-rollbar@0.4.11 + - @backstage/plugin-airbrake@0.3.11 + - @backstage/plugin-azure-devops@0.2.2 + - @backstage/plugin-badges@0.2.35 + - @internal/plugin-catalog-customized@0.0.4 + - @backstage/plugin-gocd@0.1.17 + - @backstage/plugin-home@0.4.27 + - @backstage/plugin-lighthouse@0.3.11 + - @backstage/plugin-newrelic-dashboard@0.2.4 + - @backstage/plugin-search@1.0.4 + - @backstage/plugin-sentry@0.4.4 + - @backstage/plugin-todo@0.2.13 + - @backstage/app-defaults@1.0.8 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-gcp-projects@0.3.30 + - @backstage/plugin-graphiql@0.2.43 + - @backstage/plugin-newrelic@0.3.29 + - @backstage/plugin-search-react@1.2.1 + - @backstage/plugin-shortcuts@0.3.3 + - @backstage/plugin-user-settings@0.5.1 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-permission-react@0.4.7 + - @backstage/plugin-search-common@1.1.1 + +## 0.2.77-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.8.0-next.1 + - @backstage/plugin-cost-insights@0.12.0-next.1 + - @backstage/plugin-techdocs@1.4.0-next.2 + - @backstage/plugin-explore@0.3.42-next.1 + - @backstage/plugin-github-actions@0.5.11-next.1 + - @backstage/cli@0.21.0-next.1 + - @backstage/plugin-catalog-graph@0.2.23-next.1 + - @backstage/core-components@0.12.0-next.1 + - @backstage/plugin-code-coverage@0.2.4-next.1 + - @backstage/plugin-org@0.6.0-next.1 + - @backstage/plugin-kubernetes@0.7.4-next.1 + - @backstage/app-defaults@1.0.8-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-airbrake@0.3.11-next.1 + - @backstage/plugin-apache-airflow@0.2.4-next.1 + - @backstage/plugin-api-docs@0.8.11-next.1 + - @backstage/plugin-azure-devops@0.2.2-next.1 + - @backstage/plugin-azure-sites@0.1.0-next.1 + - @backstage/plugin-badges@0.2.35-next.1 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-catalog-import@0.9.1-next.1 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-circleci@0.3.11-next.1 + - @backstage/plugin-cloudbuild@0.3.11-next.1 + - @backstage/plugin-dynatrace@1.0.1-next.1 + - @backstage/plugin-gcalendar@0.3.7-next.1 + - @backstage/plugin-gcp-projects@0.3.30-next.1 + - @backstage/plugin-gocd@0.1.17-next.1 + - @backstage/plugin-graphiql@0.2.43-next.1 + - @backstage/plugin-home@0.4.27-next.1 + - @backstage/plugin-jenkins@0.7.10-next.1 + - @backstage/plugin-kafka@0.3.11-next.1 + - @backstage/plugin-lighthouse@0.3.11-next.1 + - @backstage/plugin-newrelic@0.3.29-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.4-next.1 + - @backstage/plugin-pagerduty@0.5.4-next.1 + - @backstage/plugin-permission-react@0.4.7-next.0 + - @backstage/plugin-playlist@0.1.2-next.2 + - @backstage/plugin-rollbar@0.4.11-next.1 + - @backstage/plugin-search@1.0.4-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + - @backstage/plugin-sentry@0.4.4-next.1 + - @backstage/plugin-shortcuts@0.3.3-next.1 + - @backstage/plugin-stack-overflow@0.1.7-next.1 + - @backstage/plugin-tech-insights@0.3.3-next.1 + - @backstage/plugin-tech-radar@0.5.18-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.6-next.1 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + - @backstage/plugin-todo@0.2.13-next.1 + - @backstage/plugin-user-settings@0.5.1-next.1 + - @internal/plugin-catalog-customized@0.0.4-next.1 + +## 0.2.77-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0-next.1 + - @backstage/plugin-playlist@0.1.2-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.6-next.0 + ## 0.2.77-next.0 ### Patch Changes diff --git a/packages/app/cypress/integration/app.js b/packages/app/cypress/integration/app.js index 7b874907be..6371bcc4f8 100644 --- a/packages/app/cypress/integration/app.js +++ b/packages/app/cypress/integration/app.js @@ -25,14 +25,17 @@ describe('App', () => { it('should display support info when clicking the button', () => { cy.visit('/'); + // eslint-disable-next-line testing-library/await-async-query cy.findByTestId('support-button').click({ force: true }); cy.contains('#backstage'); }); it('should display error message when triggering it', () => { cy.visit('/'); + // eslint-disable-next-line testing-library/await-async-query cy.findByTestId('error-button').click({ force: true }); cy.contains('Error: Oh no!'); + // eslint-disable-next-line testing-library/await-async-query cy.findByTestId('error-button-close').click({ force: true }); }); @@ -55,6 +58,7 @@ describe('App', () => { cy.contains('Token') .get('input[name=github-auth-tf]') .type('password', { force: true }); + // eslint-disable-next-line testing-library/await-async-query cy.findByTestId('github-auth-button').click({ force: true }); cy.contains(`Welcome, ${name}!`); cy.contains('Logout').click({ force: true }); diff --git a/packages/app/package.json b/packages/app/package.json index 9388f305f6..3f15471993 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.77-next.0", + "version": "0.2.78-next.1", "private": true, "backstage": { "role": "frontend" @@ -80,7 +80,7 @@ "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-use": "^17.2.4", - "zen-observable": "^0.8.15" + "zen-observable": "^0.10.0" }, "devDependencies": { "@backstage/test-utils": "workspace:^", diff --git a/packages/app/public/index.html b/packages/app/public/index.html index 885fb6c228..0710a0d69d 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -44,24 +44,7 @@ /> <%= config.getString('app.title') %> - <% if (config.has('app.googleAnalyticsTrackingId')) { %> - - - <% } %> <% if (config.has('app.datadogRum')) { %> + <% if (config.has('app.datadogRum')) { %> + - + - <% } %> + + ``` + + Additionally, you should make the following adjustment to `app-config.yaml`: + + ```diff + app: + title: Backstage Example App + baseUrl: http://localhost:3000 + - googleAnalyticsTrackingId: # UA-000000-0 + ``` + +- 4091c73e68: Updated `@swc/core` to `v1.3.9` which fixes a `.tsx` parser bug. You may want to run `yarn backstage-cli versions:bump` to get on latest version including the CLI itself. +- 80bfac5266: Updated the create-app command to no longer require Git to be installed and configured. A git repository will only be initialized if possible and if not already in an git repository. +- 286d474675: The `build` script in the root `package.json` has been split into two separate scripts, `build:backend` and `build:all`. This is to more accurately reflect what is being built, to avoid confusion. + + If you want to build the project for a production deployment, you will want to use `build:backend`, as this builds both the frontend and backend package. If you are not using the `app-backend` plugin you will want to add your own `build:frontend` script, to which you can pass additional configuration parameters if needed. + + The `build:all` script is useful if you simply want to check that it is possible to build all packages in the project. This might be useful as a CI check, but is generally unnecessary. + + If you want to publish the packages in your repository you can add a `build:packages` script that calls `backstage-cli repo build`. This will skip the frontend and backend packages builds, as those are quite time consuming. + + To apply these changes to an existing project, make the following change to the root `package.json` file: + + ```diff + - "build": "backstage-cli repo build --all", + + "build:backend": "yarn workspace backend build", + + "build:all": "backstage-cli repo build --all", + ``` + + There are also a couple of places where documentation has been updated, see the [upgrade helper](https://backstage.github.io/upgrade-helper/?to=1.8.0) for a full list of changes. + +- 384eaa2307: Switched Node.js version to support version 16 & 18, rather than 14 & 16. To switch the Node.js version in your own project, apply the following change to the root `package.json`: + + ```diff + "engines": { + - "node": "14 || 16" + + "node": "16 || 18" + }, + ``` + + As well as the following change to `packages/app/package.json`: + + ```diff + - "@types/node": "^14.14.32", + + "@types/node": "^16.11.26", + ``` + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- 864c876e57: Fixed incorrect comments in the templated `app-config.yaml` and `app-config.production.yaml`. The `backend.listen` directive is not in fact needed to override the `backend.baseUrl`, the backend listens to all interfaces by default. The configuration has also been updated to listen to all interfaces, rather than just IPv4 ones, as this is required for Node.js v18. The production configuration now also shows the option to specify `backend.listen` as a single string. + + To apply this changes to an existing app, make the following change to `app-config.yaml`: + + ```diff + - # Uncomment the following host directive to bind to all IPv4 interfaces and + - # not just the baseUrl hostname. + - # host: 0.0.0.0 + + # Uncomment the following host directive to bind to specific interfaces + + # host: 127.0.0.1 + ``` + + And the following change to `app-config.production.yaml`: + + ```diff + - listen: + - port: 7007 + - # The following host directive binds to all IPv4 interfaces when its value + - # is "0.0.0.0". This is the most permissive setting. The right value depends + - # on your specific deployment. If you remove the host line entirely, the + - # backend will bind on the interface that corresponds to the backend.baseUrl + - # hostname. + - host: 0.0.0.0 + + # The listener can also be expressed as a single : string. In this case we bind to + + # all interfaces, the most permissive setting. The right value depends on your specific deployment. + + listen: ':7007' + ``` + +- Updated dependencies + - @backstage/cli-common@0.1.10 + +## 0.4.34-next.2 + +### Patch Changes + +- 384eaa2307: Switched Node.js version to support version 16 & 18, rather than 14 & 16. To switch the Node.js version in your own project, apply the following change to the root `package.json`: + + ```diff + "engines": { + - "node": "14 || 16" + + "node": "16 || 18" + }, + ``` + + As well as the following change to `packages/app/package.json`: + + ```diff + - "@types/node": "^14.14.32", + + "@types/node": "^16.11.26", + ``` + +- 864c876e57: Fixed incorrect comments in the templated `app-config.yaml` and `app-config.production.yaml`. The `backend.listen` directive is not in fact needed to override the `backend.baseUrl`, the backend listens to all interfaces by default. The configuration has also been updated to listen to all interfaces, rather than just IPv4 ones, as this is required for Node.js v18. The production configuration now also shows the option to specify `backend.listen` as a single string. + + To apply this changes to an existing app, make the following change to `app-config.yaml`: + + ```diff + - # Uncomment the following host directive to bind to all IPv4 interfaces and + - # not just the baseUrl hostname. + - # host: 0.0.0.0 + + # Uncomment the following host directive to bind to specific interfaces + + # host: 127.0.0.1 + ``` + + And the following change to `app-config.production.yaml`: + + ```diff + - listen: + - port: 7007 + - # The following host directive binds to all IPv4 interfaces when its value + - # is "0.0.0.0". This is the most permissive setting. The right value depends + - # on your specific deployment. If you remove the host line entirely, the + - # backend will bind on the interface that corresponds to the backend.baseUrl + - # hostname. + - host: 0.0.0.0 + + # The listener can also be expressed as a single : string. In this case we bind to + + # all interfaces, the most permissive setting. The right value depends on your specific deployment. + + listen: ':7007' + ``` + +- Updated dependencies + - @backstage/cli-common@0.1.10 + +## 0.4.33-next.1 + +### Patch Changes + +- Bumped create-app version. + ## 0.4.33-next.0 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index a2a168b263..e233e67ea5 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.4.33-next.0", + "version": "0.4.35-next.1", "publishConfig": { "access": "public" }, diff --git a/packages/create-app/templates/default-app/app-config.production.yaml b/packages/create-app/templates/default-app/app-config.production.yaml index 6535d967d5..df09dac50a 100644 --- a/packages/create-app/templates/default-app/app-config.production.yaml +++ b/packages/create-app/templates/default-app/app-config.production.yaml @@ -9,14 +9,9 @@ backend: # callers. When its value is "http://localhost:7007", it's strictly private # and can't be reached by others. baseUrl: http://localhost:7007 - listen: - port: 7007 - # The following host directive binds to all IPv4 interfaces when its value - # is "0.0.0.0". This is the most permissive setting. The right value depends - # on your specific deployment. If you remove the host line entirely, the - # backend will bind on the interface that corresponds to the backend.baseUrl - # hostname. - host: 0.0.0.0 + # The listener can also be expressed as a single : string. In this case we bind to + # all interfaces, the most permissive setting. The right value depends on your specific deployment. + listen: ':7007' # config options: https://node-postgres.com/api/client database: 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 4a058deefd..1a45d4015b 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -15,9 +15,8 @@ backend: baseUrl: http://localhost:7007 listen: port: 7007 - # Uncomment the following host directive to bind to all IPv4 interfaces and - # not just the baseUrl hostname. - # host: 0.0.0.0 + # Uncomment the following host directive to bind to specific interfaces + # host: 127.0.0.1 csp: connect-src: ["'self'", 'http:', 'https:'] # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index c5810d8880..070cb871bc 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -3,13 +3,14 @@ "version": "1.0.0", "private": true, "engines": { - "node": "14 || 16" + "node": "16 || 18" }, "scripts": { "dev": "concurrently \"yarn start\" \"yarn start-backend\"", "start": "yarn workspace app start", "start-backend": "yarn workspace backend start", - "build": "backstage-cli repo build --all", + "build:backend": "yarn workspace backend build", + "build:all": "backstage-cli repo build --all", "build-image": "yarn workspace backend build-image", "tsc": "tsc", "tsc:full": "tsc --skipLibCheck false --incremental false", diff --git a/packages/create-app/templates/default-app/packages/app/public/index.html b/packages/create-app/templates/default-app/packages/app/public/index.html index a936c73602..c6083b3dd2 100644 --- a/packages/create-app/templates/default-app/packages/app/public/index.html +++ b/packages/create-app/templates/default-app/packages/app/public/index.html @@ -43,24 +43,6 @@ color="#5bbad5" /> <%= config.getString('app.title') %> - <% if (config.has('app.googleAnalyticsTrackingId')) { %> - - - <% } %> @@ -72,8 +54,8 @@ You can add webfonts, meta tags, or analytics to this file. The build step will place the bundled scripts into the tag. - To begin the development, run `npm start` or `yarn start`. - To create a production bundle, use `npm run build` or `yarn build`. + To begin the development, run `yarn start`. + To create a production bundle, use `yarn build`. --> diff --git a/packages/create-app/templates/default-app/packages/backend/Dockerfile b/packages/create-app/templates/default-app/packages/backend/Dockerfile index f0be3ac1d3..51f7124db4 100644 --- a/packages/create-app/templates/default-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/default-app/packages/backend/Dockerfile @@ -5,7 +5,7 @@ # # yarn install # yarn tsc -# yarn build +# yarn build:backend # # Once the commands have been run, you can build the image using `yarn build-image` diff --git a/packages/create-app/templates/default-app/packages/backend/README.md b/packages/create-app/templates/default-app/packages/backend/README.md index 02426ef9b3..aa042db030 100644 --- a/packages/create-app/templates/default-app/packages/backend/README.md +++ b/packages/create-app/templates/default-app/packages/backend/README.md @@ -16,8 +16,6 @@ To run the example backend, first go to the project root and run ```bash yarn install -yarn tsc -yarn build ``` You should only need to do this once. @@ -25,16 +23,11 @@ You should only need to do this once. After that, go to the `packages/backend` directory and run ```bash -AUTH_GOOGLE_CLIENT_ID=x AUTH_GOOGLE_CLIENT_SECRET=x \ -AUTH_GITHUB_CLIENT_ID=x AUTH_GITHUB_CLIENT_SECRET=x \ -AUTH_OAUTH2_CLIENT_ID=x AUTH_OAUTH2_CLIENT_SECRET=x \ -AUTH_OAUTH2_AUTH_URL=x AUTH_OAUTH2_TOKEN_URL=x \ -LOG_LEVEL=debug \ yarn start ``` -Substitute `x` for actual values, or leave them as dummy values just to try out -the backend without using the auth or sentry features. +If you want to override any configuration locally, for example adding any secrets, +you can do so in `app-config.local.yaml`. The backend starts up on port 7007 per default. diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 7177a25fb2..f3a505fc02 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/dev-utils +## 1.0.9-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/test-utils@1.2.3-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/app-defaults@1.0.9-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 1.0.9-next.0 + +### Patch Changes + +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/test-utils@1.2.3-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/app-defaults@1.0.9-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 1.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-app-api@1.2.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/app-defaults@1.0.8 + - @backstage/integration-react@1.1.6 + - @backstage/test-utils@1.2.2 + - @backstage/theme@0.2.16 + +## 1.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/app-defaults@1.0.8-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/test-utils@1.2.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 1.0.8-next.0 ### Patch Changes @@ -850,7 +912,7 @@ ### Patch Changes -- 5aa4ceea6: Make sure to provide dummy routes for all external routes of plugins given to DevApp +- 5aa4ceea6: Make sure to provide sample routes for all external routes of plugins given to DevApp - Updated dependencies [3a58084b6] - Updated dependencies [e799e74d4] - Updated dependencies [dc12852c9] diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 0decdbe985..1f7578f837 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "1.0.8-next.0", + "version": "1.0.9-next.1", "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -47,7 +47,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "react-use": "^17.2.4", - "zen-observable": "^0.8.15" + "zen-observable": "^0.10.0" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index e04779b4d1..6fa73ba860 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -165,9 +165,9 @@ export class DevAppBuilder { * Build a DevApp component using the resources registered so far */ build(): ComponentType<{}> { - const dummyRouteRef = createRouteRef({ id: 'dummy' }); - const DummyPage = () => Page belonging to another plugin.; - attachComponentData(DummyPage, 'core.mountPoint', dummyRouteRef); + const fakeRouteRef = createRouteRef({ id: 'fake' }); + const FakePage = () => Page belonging to another plugin.; + attachComponentData(FakePage, 'core.mountPoint', fakeRouteRef); const apis = [...this.apis]; if (!apis.some(api => api.api.id === scmIntegrationsApiRef.id)) { @@ -188,7 +188,7 @@ export class DevAppBuilder { for (const plugin of this.plugins ?? []) { const targets: Record> = {}; for (const routeKey of Object.keys(plugin.externalRoutes)) { - targets[routeKey] = dummyRouteRef; + targets[routeKey] = fakeRouteRef; } bind(plugin.externalRoutes, targets); } @@ -215,7 +215,7 @@ export class DevAppBuilder { {this.routes} - } /> + } /> diff --git a/packages/e2e-test/README.md b/packages/e2e-test/README.md index 139213c0c1..f3692de2a7 100644 --- a/packages/e2e-test/README.md +++ b/packages/e2e-test/README.md @@ -11,7 +11,7 @@ All packages need to be installed and built before running the test. In a fresh ```sh yarn install yarn tsc -yarn build +yarn build:all ``` Once those tasks have completed, you can now run the test using `yarn e2e-test run`. diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 3f9d28eadd..fe4d87bea4 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -275,7 +275,7 @@ async function createApp( for (const cmd of [ 'install', 'tsc:full', - 'build', + 'build:all', 'lint:all', 'prettier:check', 'test:all', @@ -507,6 +507,7 @@ async function testBackendStart(appDir: string, ...args: string[]) { // Skipping the whole block throw new Error(stderr); } + await new Promise(resolve => setTimeout(resolve, 500)); print('Try to fetch entities from the backend'); // Try fetch entities, should be ok diff --git a/packages/errors/CHANGELOG.md b/packages/errors/CHANGELOG.md index f64792a5a3..5a46b58378 100644 --- a/packages/errors/CHANGELOG.md +++ b/packages/errors/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/errors +## 1.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + +## 1.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.0 + +## 1.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1 + ## 1.1.3-next.0 ### Patch Changes diff --git a/packages/errors/package.json b/packages/errors/package.json index 9b9e0dcd1d..30656c6d41 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/errors", "description": "Common utilities for error handling within Backstage", - "version": "1.1.3-next.0", + "version": "1.1.4-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index b396593d12..871243fb94 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/integration-react +## 1.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/theme@0.2.16 + +## 1.1.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## 1.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## 1.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + ## 1.1.6-next.0 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 7efea2e74a..239cd87a70 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "1.1.6-next.0", + "version": "1.1.7-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -45,7 +45,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 4c34e91f8d..0542c17989 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/integration +## 1.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## 1.4.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 34b039ca9f: Added `integrations.github.apps.allowedInstallationOwners` to the configuration schema. +- Updated dependencies + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 1.4.0 + +### Minor Changes + +- d05e1841ce: This patch brings Gitea as a valid integration: target, via the ScmIntegration interface. It adds gitea to the relevant static properties (get integration by name, get integration by type) for plugins to be able to reference the same Gitea server. +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. + + Deprecates: + + - `getGitHubFileFetchUrl` replaced by `getGithubFileFetchUrl` + - `GitHubIntegrationConfig` replaced by `GithubIntegrationConfig` + - `GitHubIntegration` replaced by `GithubIntegration` + - `readGitHubIntegrationConfig` replaced by `readGithubIntegrationConfig` + - `readGitHubIntegrationConfigs` replaced by `readGithubIntegrationConfigs` + - `replaceGitHubUrlType` replaced by `replaceGithubUrlType` + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- a6d779d58a: Remove explicit default visibility at `config.d.ts` files. + + ```ts + /** + * @visibility backend + */ + ``` + +- Updated dependencies + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + ## 1.4.0-next.0 ### Minor Changes diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index 49a121aff3..a1eb7aceb9 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -189,6 +189,14 @@ export interface Config { * @visibility secret */ clientSecret: string; + /** + * List of installation owners allowed to be used by this GitHub app. The GitHub UI does not provide a way to list the installations. + * However you can list the installations with the GitHub API. You can find the list of installations here: + * https://api.github.com/app/installations + * The relevant documentation for this is here. + * https://docs.github.com/en/rest/reference/apps#list-installations-for-the-authenticated-app--code-samples + */ + allowedInstallationOwners?: string[]; }>; }>; diff --git a/packages/integration/package.json b/packages/integration/package.json index bbed7de2d1..bcf6da2d74 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration", "description": "Helpers for managing integrations towards external systems", - "version": "1.4.0-next.0", + "version": "1.4.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -46,7 +46,7 @@ "@backstage/config-loader": "workspace:^", "@backstage/test-utils": "workspace:^", "@types/luxon": "^3.0.0", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/packages/release-manifests/CHANGELOG.md b/packages/release-manifests/CHANGELOG.md index 177ea55a5a..e8bb4dfb07 100644 --- a/packages/release-manifests/CHANGELOG.md +++ b/packages/release-manifests/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/release-manifests +## 0.0.8-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. + +## 0.0.7 + +### Patch Changes + +- a4496131fa: Added a fallback that fetches manifests from `https://raw.githubusercontent.com` if `https://versions.backstage.io` is unavailable. + +## 0.0.7-next.0 + +### Patch Changes + +- a4496131fa: Added a fallback that fetches manifests from `https://raw.githubusercontent.com` if `https://versions.backstage.io` is unavailable. + ## 0.0.6 ### Patch Changes diff --git a/packages/release-manifests/package.json b/packages/release-manifests/package.json index 949cd2bcd5..2a53438bd2 100644 --- a/packages/release-manifests/package.json +++ b/packages/release-manifests/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/release-manifests", "description": "Helper library for receiving release manifests", - "version": "0.0.6", + "version": "0.0.8-next.0", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -38,7 +38,7 @@ "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", "@types/node": "^16.0.0", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/packages/release-manifests/src/manifest.test.ts b/packages/release-manifests/src/manifest.test.ts index 272555df70..6a53f993c8 100644 --- a/packages/release-manifests/src/manifest.test.ts +++ b/packages/release-manifests/src/manifest.test.ts @@ -17,7 +17,11 @@ import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { setupRequestMockHandlers } from '@backstage/test-utils'; -import { getManifestByReleaseLine, getManifestByVersion } from './manifest'; +import { + getManifestByReleaseLine, + getManifestByVersion, + withFallback, +} from './manifest'; describe('Release Manifests', () => { const worker = setupServer(); @@ -86,3 +90,50 @@ describe('Release Manifests', () => { }); }); }); + +describe('withFallback', () => { + it('should use the first value to resolve', async () => { + const fn1 = jest.fn((_s: AbortSignal) => Promise.resolve(1)); + const fn2 = jest.fn((_s: AbortSignal) => Promise.resolve(2)); + await expect(withFallback(fn1, fn2, 100)).resolves.toBe(1); + expect(fn1.mock.lastCall?.[0].aborted).toBe(false); + expect(fn2).not.toHaveBeenCalled(); + }); + + it('should fall back on rejection', async () => { + const fn1 = jest.fn((_s: AbortSignal) => Promise.reject(new Error('1'))); + const fn2 = jest.fn((_s: AbortSignal) => Promise.resolve(2)); + await expect(withFallback(fn1, fn2, 0)).resolves.toBe(2); + expect(fn1.mock.lastCall?.[0].aborted).toBe(true); + expect(fn2.mock.lastCall?.[0].aborted).toBe(false); + }); + + it('should fall back on timeout', async () => { + const fn1 = jest.fn((_s: AbortSignal) => new Promise(() => {})); + const fn2 = jest.fn((_s: AbortSignal) => Promise.resolve(2)); + await expect(withFallback(fn1, fn2, 0)).resolves.toBe(2); + expect(fn1.mock.lastCall?.[0].aborted).toBe(true); + expect(fn2.mock.lastCall?.[0].aborted).toBe(false); + }); + + it('should always reject with the first error', async () => { + const fn1 = jest.fn((_s: AbortSignal) => Promise.reject(new Error('1'))); + const fn2 = jest.fn((_s: AbortSignal) => Promise.reject(new Error('2'))); + await expect(withFallback(fn1, fn2, 0)).rejects.toThrow('1'); + expect(fn1.mock.lastCall?.[0].aborted).toBe(false); + expect(fn2.mock.lastCall?.[0].aborted).toBe(false); + }); + + it('should always reject with the first error even if rejected after', async () => { + const fn1 = jest.fn( + (_s: AbortSignal) => + new Promise((_resolve, reject) => { + setTimeout(() => reject(new Error('1')), 100); + }), + ); + const fn2 = jest.fn((_s: AbortSignal) => Promise.reject(new Error('2'))); + await expect(withFallback(fn1, fn2, 0)).rejects.toThrow('1'); + expect(fn1.mock.lastCall?.[0].aborted).toBe(false); + expect(fn2.mock.lastCall?.[0].aborted).toBe(false); + }); +}); diff --git a/packages/release-manifests/src/manifest.ts b/packages/release-manifests/src/manifest.ts index 955cf7308c..004ccfc77c 100644 --- a/packages/release-manifests/src/manifest.ts +++ b/packages/release-manifests/src/manifest.ts @@ -16,7 +16,9 @@ import fetch from 'cross-fetch'; -const VERSIONS_DOMAIN = 'https://versions.backstage.io'; +const VERSIONS_BASE_URL = 'https://versions.backstage.io'; +const GITHUB_RAW_BASE_URL = + 'https://raw.githubusercontent.com/backstage/versions/main'; /** * Contains mapping between Backstage release and package versions. @@ -35,6 +37,45 @@ export type GetManifestByVersionOptions = { version: string; }; +// Wait for waitMs, or until signal is aborted. +function wait(waitMs: number, signal: AbortSignal) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (!signal.aborted) { + resolve(); + } + }, waitMs); + signal.addEventListener('abort', () => { + clearTimeout(timeout); + reject(new Error('Aborted')); + }); + }); +} + +// Run fn1 and then fn2 after fallbackDelayMs. Whichever one finishes +// first wins, and the other one is aborted through the provided signal. +export async function withFallback( + fn1: (signal: AbortSignal) => Promise, + fn2: (signal: AbortSignal) => Promise, + fallbackDelayMs: number, +): Promise { + const c1 = new AbortController(); + const c2 = new AbortController(); + + const promise1 = fn1(c1.signal).then(res => { + c2.abort(); + return res; + }); + const promise2 = wait(fallbackDelayMs, c2.signal) + .then(() => fn2(c2.signal)) + .then(res => { + c1.abort(); + return res; + }); + + return Promise.any([promise1, promise2]).catch(() => promise1); +} + /** * Returns a release manifest based on supplied version. * @public @@ -42,19 +83,27 @@ export type GetManifestByVersionOptions = { export async function getManifestByVersion( options: GetManifestByVersionOptions, ): Promise { - const url = `${VERSIONS_DOMAIN}/v1/releases/${encodeURIComponent( - options.version, - )}/manifest.json`; - const response = await fetch(url); - if (response.status === 404) { + const versionEnc = encodeURIComponent(options.version); + const res = await withFallback( + signal => + fetch(`${VERSIONS_BASE_URL}/v1/releases/${versionEnc}/manifest.json`, { + signal, + }), + signal => + fetch(`${GITHUB_RAW_BASE_URL}/v1/releases/${versionEnc}/manifest.json`, { + signal, + }), + 500, + ); + if (res.status === 404) { throw new Error(`No release found for ${options.version} version`); } - if (response.status !== 200) { + if (res.status !== 200) { throw new Error( - `Unexpected response status ${response.status} when fetching release from ${url}.`, + `Unexpected response status ${res.status} when fetching release from ${res.url}.`, ); } - return await response.json(); + return res.json(); } /** @@ -72,17 +121,31 @@ export type GetManifestByReleaseLineOptions = { export async function getManifestByReleaseLine( options: GetManifestByReleaseLineOptions, ): Promise { - const url = `${VERSIONS_DOMAIN}/v1/tags/${encodeURIComponent( - options.releaseLine, - )}/manifest.json`; - const response = await fetch(url); - if (response.status === 404) { + const releaseEnc = encodeURIComponent(options.releaseLine); + const res = await withFallback( + signal => + fetch(`${VERSIONS_BASE_URL}/v1/tags/${releaseEnc}/manifest.json`, { + signal, + }), + async signal => { + // The release tags are symlinks, which we need to follow manually when fetching from GitHub. + const baseUrl = `${GITHUB_RAW_BASE_URL}/v1/tags/${releaseEnc}`; + const linkRes = await fetch(baseUrl, { signal }); + if (!linkRes.ok) { + return linkRes; + } + const link = (await linkRes.text()).trim(); + return fetch(new URL(`${link}/manifest.json`, baseUrl), { signal }); + }, + 1000, + ); + if (res.status === 404) { throw new Error(`No '${options.releaseLine}' release line found`); } - if (response.status !== 200) { + if (res.status !== 200) { throw new Error( - `Unexpected response status ${response.status} when fetching release from ${url}.`, + `Unexpected response status ${res.status} when fetching release from ${res.url}.`, ); } - return await response.json(); + return res.json(); } diff --git a/packages/repo-tools/.eslintrc.js b/packages/repo-tools/.eslintrc.js new file mode 100644 index 0000000000..599478bcc8 --- /dev/null +++ b/packages/repo-tools/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'no-console': 0, + }, +}); diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md new file mode 100644 index 0000000000..2aa40b7cb3 --- /dev/null +++ b/packages/repo-tools/CHANGELOG.md @@ -0,0 +1,27 @@ +# @backstage/repo-tools + +## 0.1.0-next.1 + +### Minor Changes + +- 03843259b4: Api reference documentation improvements + + - breadcrumbs links semantics as code spans + - new `@config` annotation to describe related config keys + +### Patch Changes + +- 71f80eb354: add the command type-deps to the repo tool package. +- Updated dependencies + - @backstage/errors@1.1.4-next.1 + +## 0.1.0-next.0 + +### Minor Changes + +- 99713fd671: Introducing repo-tools package + +### Patch Changes + +- Updated dependencies + - @backstage/errors@1.1.4-next.0 diff --git a/packages/repo-tools/README.md b/packages/repo-tools/README.md new file mode 100644 index 0000000000..eb9f70dd72 --- /dev/null +++ b/packages/repo-tools/README.md @@ -0,0 +1,11 @@ +# @backstage/repo-tools + +This package provides a CLI for backstage repo tooling. + +## Installation + +Install the package via Yarn: + +```sh +yarn add @backstage/repo-tools +``` diff --git a/packages/repo-tools/bin/backstage-repo-tools b/packages/repo-tools/bin/backstage-repo-tools new file mode 100755 index 0000000000..377e4a32ca --- /dev/null +++ b/packages/repo-tools/bin/backstage-repo-tools @@ -0,0 +1,37 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('path'); + +// Figure out whether we're running inside the backstage repo or as an installed dependency +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src')); + +if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) { + require('..'); +} else { + require('ts-node').register({ + transpileOnly: true, + /* eslint-disable-next-line no-restricted-syntax */ + project: path.resolve(__dirname, '../../../tsconfig.json'), + compilerOptions: { + module: 'CommonJS', + }, + }); + + require('../src'); +} diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md new file mode 100644 index 0000000000..309ddad2a3 --- /dev/null +++ b/packages/repo-tools/cli-report.md @@ -0,0 +1,39 @@ +## CLI Report file for "@backstage/repo-tools" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-repo-tools` + +``` +Usage: backstage-repo-tools [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + api-reports [options] [path...] + type-deps + help [command] +``` + +### `backstage-repo-tools api-reports` + +``` +Usage: backstage-repo-tools api-reports [options] [path...] + +Options: + --ci + --tsc + --docs + -h, --help +``` + +### `backstage-repo-tools type-deps` + +``` +Usage: backstage-repo-tools type-deps [options] + +Options: + -h, --help +``` diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json new file mode 100644 index 0000000000..a0caa3ee9c --- /dev/null +++ b/packages/repo-tools/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/repo-tools", + "description": "CLI for Backstage repo tooling ", + "version": "0.1.0-next.1", + "publishConfig": { + "access": "public" + }, + "backstage": { + "role": "cli" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/repo-tools" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "dist/index.cjs.js", + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "start": "nodemon --" + }, + "bin": { + "backstage-repo-tools": "bin/backstage-repo-tools" + }, + "dependencies": { + "@backstage/errors": "workspace:^", + "@manypkg/get-packages": "^1.1.3", + "@microsoft/api-documenter": "^7.17.11", + "@microsoft/api-extractor": "^7.23.0", + "@microsoft/api-extractor-model": "^7.17.2", + "@microsoft/tsdoc": "0.14.1", + "chalk": "^4.0.0", + "commander": "^9.1.0", + "fs-extra": "10.1.0", + "ts-node": "^10.0.0" + }, + "files": [ + "bin", + "dist/**/*.js" + ], + "nodemonConfig": { + "watch": "./src", + "exec": "bin/backstage-repo-tools", + "ext": "ts" + } +} diff --git a/scripts/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts similarity index 84% rename from scripts/api-extractor.ts rename to packages/repo-tools/src/commands/api-reports/api-extractor.ts index 244008584f..bae62ccc7e 100644 --- a/scripts/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -24,7 +24,7 @@ import { dirname, join, } from 'path'; -import { spawnSync, execFile } from 'child_process'; +import { execFile } from 'child_process'; import prettier from 'prettier'; import fs from 'fs-extra'; import { @@ -39,9 +39,19 @@ import { DocNode, IDocNodeContainerParameters, TSDocTagSyntaxKind, + TSDocConfiguration, + Standardization, + DocBlockTag, + DocPlainText, + DocLinkTag, } from '@microsoft/tsdoc'; import { TSDocConfigFile } from '@microsoft/tsdoc-config'; -import { ApiPackage, ApiModel, ApiItem } from '@microsoft/api-extractor-model'; +import { + ApiPackage, + ApiModel, + ApiItem, + ApiItemKind, +} from '@microsoft/api-extractor-model'; import { IMarkdownDocumenterOptions, MarkdownDocumenter, @@ -49,11 +59,17 @@ import { import { DocTable } from '@microsoft/api-documenter/lib/nodes/DocTable'; import { DocTableRow } from '@microsoft/api-documenter/lib/nodes/DocTableRow'; import { DocHeading } from '@microsoft/api-documenter/lib/nodes/DocHeading'; -import { CustomMarkdownEmitter } from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; +import { + CustomMarkdownEmitter, + ICustomMarkdownEmitterOptions, +} from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/MarkdownEmitter'; import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration'; -const tmpDir = resolvePath(__dirname, '../node_modules/.cache/api-extractor'); +const tmpDir = resolvePath( + process.cwd(), + './node_modules/.cache/api-extractor', +); /** * All of this monkey patching below is because MUI has these bare package.json file as a method @@ -99,7 +115,9 @@ function patchFileMessageFetcher( } = router; router.fetchAssociatedMessagesForReviewFile = - function patchedFetchAssociatedMessagesForReviewFile(ast) { + function patchedFetchAssociatedMessagesForReviewFile( + ast: AstDeclaration | undefined, + ) { const messages = fetchAssociatedMessagesForReviewFile.call(this, ast); return transform(messages, ast); }; @@ -113,7 +131,10 @@ function patchFileMessageFetcher( const originalGenerateReviewFileContent = ApiReportGenerator.generateReviewFileContent; ApiReportGenerator.generateReviewFileContent = - function decoratedGenerateReviewFileContent(collector, ...moreArgs) { + function decoratedGenerateReviewFileContent( + collector: { program: Program; messageRouter: any }, + ...moreArgs: any[] + ) { const program = collector.program as Program; // The purpose of this override is to allow the @ignore tag to be used to ignore warnings @@ -137,7 +158,9 @@ ApiReportGenerator.generateReviewFileContent = } const [, symbolName] = symbolMatch; - const sourceFile = program.getSourceFile(message.sourceFilePath); + const sourceFile = + message.sourceFilePath && + program.getSourceFile(message.sourceFilePath); if (!sourceFile) { throw new Error( `Failed to find source file in program at path "${message.sourceFilePath}"`, @@ -210,7 +233,7 @@ const ALLOW_WARNINGS = [ async function resolvePackagePath( packagePath: string, ): Promise { - const projectRoot = resolvePath(__dirname, '..'); + const projectRoot = resolvePath(process.cwd()); const fullPackageDir = resolvePath(projectRoot, packagePath); const stat = await fs.stat(fullPackageDir); @@ -228,7 +251,7 @@ async function resolvePackagePath( return relativePath(projectRoot, fullPackageDir); } -async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) { +export async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) { const packageDirs = new Array(); for (const unresolvedPackageDir of unresolvedPackageDirs) { @@ -246,9 +269,9 @@ async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) { return packageDirs; } -async function findPackageDirs() { +export async function findPackageDirs() { const packageDirs = new Array(); - const projectRoot = resolvePath(__dirname, '..'); + const projectRoot = resolvePath(process.cwd()); for (const packageRoot of PACKAGE_ROOTS) { const dirs = await fs.readdir(resolvePath(projectRoot, packageRoot)); @@ -265,8 +288,8 @@ async function findPackageDirs() { return packageDirs; } -async function createTemporaryTsConfig(includedPackageDirs: string[]) { - const path = resolvePath(__dirname, '..', 'tsconfig.tmp.json'); +export async function createTemporaryTsConfig(includedPackageDirs: string[]) { + const path = resolvePath(process.cwd(), 'tsconfig.tmp.json'); process.once('exit', () => { fs.removeSync(path); @@ -284,7 +307,7 @@ async function createTemporaryTsConfig(includedPackageDirs: string[]) { return path; } -async function countApiReportWarnings(projectFolder: string) { +export async function countApiReportWarnings(projectFolder: string) { const path = resolvePath(projectFolder, 'api-report.md'); try { const content = await fs.readFile(path, 'utf8'); @@ -313,7 +336,7 @@ async function countApiReportWarnings(projectFolder: string) { } } -async function getTsDocConfig() { +export async function getTsDocConfig() { const tsdocConfigFile = await TSDocConfigFile.loadFile( require.resolve('@microsoft/api-extractor/extends/tsdoc-base.json'), ); @@ -321,7 +344,12 @@ async function getTsDocConfig() { tagName: '@ignore', syntaxKind: TSDocTagSyntaxKind.ModifierTag, }); + tsdocConfigFile.addTagDefinition({ + tagName: '@config', + syntaxKind: TSDocTagSyntaxKind.BlockTag, + }); tsdocConfigFile.setSupportForTag('@ignore', true); + tsdocConfigFile.setSupportForTag('@config', true); return tsdocConfigFile; } @@ -349,7 +377,7 @@ interface ApiExtractionOptions { tsconfigFilePath: string; } -async function runApiExtraction({ +export async function runApiExtraction({ packageDirs, outputDir, isLocalBuild, @@ -358,7 +386,10 @@ async function runApiExtraction({ await fs.remove(outputDir); const entryPoints = packageDirs.map(packageDir => { - return resolvePath(__dirname, `../dist-types/${packageDir}/src/index.d.ts`); + return resolvePath( + process.cwd(), + `./dist-types/${packageDir}/src/index.d.ts`, + ); }); let compilerState: CompilerState | undefined = undefined; @@ -367,8 +398,12 @@ async function runApiExtraction({ for (const packageDir of packageDirs) { console.log(`## Processing ${packageDir}`); - const projectFolder = resolvePath(__dirname, '..', packageDir); - const packageFolder = resolvePath(__dirname, '../dist-types', packageDir); + const projectFolder = resolvePath(process.cwd(), packageDir); + const packageFolder = resolvePath( + process.cwd(), + './dist-types', + packageDir, + ); const warningCountBefore = await countApiReportWarnings(projectFolder); @@ -618,7 +653,7 @@ class ApiModelTransforms { if (serialized.members.length !== 1) { throw new Error( `Unexpected members in serialized ApiPackage, [${serialized.members - .map(m => m.kind) + .map((m: { kind: any }) => m.kind) .join(' ')}]`, ); } @@ -634,7 +669,7 @@ class ApiModelTransforms { members: [ { ...entryPoint, - members: entryPoint.members.map(member => + members: entryPoint.members.map((member: any) => transforms.reduce((m, t) => t(m), member), ), }, @@ -753,7 +788,7 @@ class ApiModelTransforms { }; } -async function buildDocs({ +export async function buildDocs({ inputDir, outputDir, }: { @@ -807,6 +842,16 @@ async function buildDocs({ } } + // This class only propose is to have a different kind and be able to render links with backticks + class DocCodeSpanLink extends DocLinkTag { + static kind = 'DocCodeSpanLink'; + + /** @override */ + public get kind(): string { + return DocCodeSpanLink.kind; + } + } + // This is where we actually write the markdown and where we can hook // in the rendering of our own nodes. class CustomCustomMarkdownEmitter extends CustomMarkdownEmitter { @@ -825,7 +870,7 @@ async function buildDocs({ /** @override */ protected writeNode( docNode: DocNode, - context: IMarkdownEmitterContext, + context: IMarkdownEmitterContext, docNodeSiblings: boolean, ): void { switch (docNode.kind) { @@ -841,13 +886,43 @@ async function buildDocs({ context.writer.writeLine(); break; } + case 'BlockTag': { + const node = docNode as DocBlockTag; + if (node.tagName === '@config') { + context.writer.writeLine('## Related config '); + } + break; + } + case DocCodeSpanLink.kind: { + const node = docNode as DocLinkTag; + if (node.codeDestination) { + // TODO @sarabadu understand if we need `codeDestination` at all on this custom DocCodeSpanLink + super.writeLinkTagWithCodeDestination(node, context); + } else if (node.urlDestination) { + const linkText = + node.linkText !== undefined ? node.linkText : node.urlDestination; + const encodedLinkText = this.getEscapedText( + linkText.replace(/\s+/g, ' '), + ); + context.writer.write('['); + context.writer.write(`\`${encodedLinkText}\``); + context.writer.write(`](${node.urlDestination})`); + } else if (node.linkText) { + this.writePlainText(node.linkText, context); + } + break; + } default: super.writeNode(docNode, context, docNodeSiblings); } } /** @override */ - emit(stringBuilder, docNode, options) { + emit( + stringBuilder: any, + docNode: DocNode, + options: ICustomMarkdownEmitterOptions, + ) { // Hack to get rid of the leading comment of each file, since // we want the front matter to come first stringBuilder._chunks.length = 0; @@ -860,15 +935,35 @@ async function buildDocs({ super(options); // It's a strict model, we gotta register the allowed usage of our new node - this._tsdocConfiguration.docNodeManager.registerDocNodes( - '@backstage/docs', - [{ docNodeKind: DocFrontMatter.kind, constructor: DocFrontMatter }], - ); - this._tsdocConfiguration.docNodeManager.registerAllowableChildren( - 'Paragraph', - [DocFrontMatter.kind], - ); + ( + this._tsdocConfiguration as TSDocConfiguration + ).docNodeManager.registerDocNodes('@backstage/docs', [ + { docNodeKind: DocFrontMatter.kind, constructor: DocFrontMatter }, + ]); + ( + this._tsdocConfiguration as TSDocConfiguration + ).docNodeManager.registerDocNodes('@backstage/docs', [ + { docNodeKind: DocCodeSpanLink.kind, constructor: DocCodeSpanLink }, + ]); + ( + this._tsdocConfiguration as TSDocConfiguration + ).docNodeManager.registerAllowableChildren('Paragraph', [ + DocFrontMatter.kind, + DocCodeSpanLink.kind, + ]); + const def = { + tagName: '@config', + syntaxKind: TSDocTagSyntaxKind.BlockTag, + tagNameWithUpperCase: '@CONFIG', + standardization: Standardization.Extended, + allowMultiple: false, + }; + (this._tsdocConfiguration as TSDocConfiguration).addTagDefinition(def); + (this._tsdocConfiguration as TSDocConfiguration).setSupportForTag( + def, + true, + ); this._markdownEmitter = new CustomCustomMarkdownEmitter(newModel); } @@ -886,7 +981,7 @@ async function buildDocs({ // so we hook in wherever we can. In this case we add the front matter // just before writing the breadcrumbs at the top. /** @override */ - _writeBreadcrumb(output, apiItem) { + _writeBreadcrumb(output: any, apiItem: ApiItem & { name: string }) { let title; let description; @@ -897,9 +992,12 @@ async function buildDocs({ } else if (apiItem.kind === 'Model') { title = 'Package Index'; description = 'Index of all Backstage Packages'; - } else { + } else if (apiItem.name) { title = apiItem.name; description = `API Reference for ${apiItem.name}`; + } else { + title = apiItem.displayName; + description = `API Reference for ${apiItem.displayName}`; } // Add our front matter @@ -914,8 +1012,40 @@ async function buildDocs({ }), ); - // Now write the actual breadcrumbs - super._writeBreadcrumb(output, apiItem); + const configuration: TSDocConfiguration = this._tsdocConfiguration; + + output.appendNodeInParagraph( + new DocLinkTag({ + configuration, + tagName: '@link', + linkText: 'Home', + urlDestination: this._getLinkFilenameForApiItem(this._apiModel), + }), + ); + + for (const hierarchyItem of apiItem.getHierarchy()) { + switch (hierarchyItem.kind) { + case ApiItemKind.Model: + case ApiItemKind.EntryPoint: + // We don't show the model as part of the breadcrumb because it is the root-level container. + // We don't show the entry point because today API Extractor doesn't support multiple entry points; + // this may change in the future. + break; + default: + output.appendNodesInParagraph([ + new DocPlainText({ + configuration, + text: ' > ', + }), + new DocCodeSpanLink({ + configuration, + tagName: '@link', + linkText: hierarchyItem.displayName, + urlDestination: this._getLinkFilenameForApiItem(hierarchyItem), + }), + ]); + } + } // We wanna ignore the header that always gets written after the breadcrumb // This otherwise becomes more or less a duplicate of the title in the front matter @@ -925,7 +1055,10 @@ async function buildDocs({ }; } - _writeModelTable(output, apiModel): void { + _writeModelTable( + output: { appendNode: (arg0: DocTable | DocHeading) => void }, + apiModel: { members: any }, + ): void { const configuration = this._tsdocConfiguration; const packagesTable = new DocTable({ @@ -998,7 +1131,10 @@ async function buildDocs({ documenter.generateFiles(); } -async function categorizePackageDirs(projectRoot, packageDirs) { +export async function categorizePackageDirs( + projectRoot: string, + packageDirs: any[], +) { const dirs = packageDirs.slice(); const tsPackageDirs = new Array(); const cliPackageDirs = new Array(); @@ -1086,11 +1222,11 @@ function parseHelpPage(helpPageContent: string) { // Trim away documentation const sectionItems = sectionLines .map(line => line.match(/^\s{1,8}(.*?)\s\s+/)?.[1]) - .filter(Boolean); + .filter(Boolean) as string[]; - if (sectionName.toLocaleLowerCase('en-US') === 'options:') { + if (sectionName?.toLocaleLowerCase('en-US') === 'options:') { options = sectionItems; - } else if (sectionName.toLocaleLowerCase('en-US') === 'commands:') { + } else if (sectionName?.toLocaleLowerCase('en-US') === 'commands:') { commands = sectionItems; } else { throw new Error(`Unknown CLI section: ${sectionName}`); @@ -1185,7 +1321,7 @@ interface CliExtractionOptions { isLocalBuild: boolean; } -async function runCliExtraction({ +export async function runCliExtraction({ projectRoot, packageDirs, isLocalBuild, @@ -1250,96 +1386,3 @@ async function runCliExtraction({ } } } - -async function main() { - const projectRoot = resolvePath(__dirname, '..'); - const isCiBuild = process.argv.includes('--ci'); - const isDocsBuild = process.argv.includes('--docs'); - const runTsc = process.argv.includes('--tsc'); - - const selectedPackageDirs = await findSpecificPackageDirs( - process.argv.slice(2).filter(arg => !arg.startsWith('--')), - ); - if (selectedPackageDirs && isCiBuild) { - throw new Error( - 'Package path arguments are not supported together with the --ci flag', - ); - } - if (!selectedPackageDirs && !isCiBuild && !isDocsBuild) { - console.log(''); - console.log( - 'TIP: You can generate api-reports for select packages by passing package paths:', - ); - console.log(''); - console.log( - ' yarn build:api-reports packages/config packages/core-plugin-api', - ); - console.log(''); - } - - let temporaryTsConfigPath: string | undefined; - if (selectedPackageDirs) { - temporaryTsConfigPath = await createTemporaryTsConfig(selectedPackageDirs); - } - const tsconfigFilePath = - temporaryTsConfigPath ?? resolvePath(projectRoot, 'tsconfig.json'); - - if (runTsc) { - await fs.remove(resolvePath(projectRoot, 'dist-types')); - const { status } = spawnSync( - 'yarn', - [ - 'tsc', - ['--project', tsconfigFilePath], - ['--skipLibCheck', 'false'], - ['--incremental', 'false'], - ].flat(), - { - stdio: 'inherit', - shell: true, - cwd: projectRoot, - }, - ); - if (status !== 0) { - process.exit(status); - } - } - - const packageDirs = selectedPackageDirs ?? (await findPackageDirs()); - - const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs( - projectRoot, - packageDirs, - ); - - if (tsPackageDirs.length > 0) { - console.log('# Generating package API reports'); - await runApiExtraction({ - packageDirs: tsPackageDirs, - outputDir: tmpDir, - isLocalBuild: !isCiBuild, - tsconfigFilePath, - }); - } - if (cliPackageDirs.length > 0) { - console.log('# Generating package CLI reports'); - await runCliExtraction({ - projectRoot, - packageDirs: cliPackageDirs, - isLocalBuild: !isCiBuild, - }); - } - - if (isDocsBuild) { - console.log('# Generating package documentation'); - await buildDocs({ - inputDir: tmpDir, - outputDir: resolvePath(projectRoot, 'docs/reference'), - }); - } -} - -main().catch(error => { - console.error(error.stack || String(error)); - process.exit(1); -}); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.ts b/packages/repo-tools/src/commands/api-reports/api-reports.ts new file mode 100644 index 0000000000..3ac740d596 --- /dev/null +++ b/packages/repo-tools/src/commands/api-reports/api-reports.ts @@ -0,0 +1,120 @@ +/* + * 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 { OptionValues } from 'commander'; +import { resolve as resolvePath } from 'path'; +import fs from 'fs-extra'; +import { spawnSync } from 'child_process'; +import { + findSpecificPackageDirs, + createTemporaryTsConfig, + findPackageDirs, + categorizePackageDirs, + runApiExtraction, + runCliExtraction, + buildDocs, +} from './api-extractor'; + +export default async (paths: string[], opts: OptionValues) => { + const tmpDir = resolvePath( + process.cwd(), + './node_modules/.cache/api-extractor', + ); + const projectRoot = resolvePath(process.cwd()); + const isCiBuild = opts.ci; + const isDocsBuild = opts.docs; + const runTsc = opts.tsc; + + const selectedPackageDirs = await findSpecificPackageDirs(paths); + + if (selectedPackageDirs && isCiBuild) { + throw new Error( + 'Package path arguments are not supported together with the --ci flag', + ); + } + if (!selectedPackageDirs && !isCiBuild && !isDocsBuild) { + console.log(''); + console.log( + 'TIP: You can generate api-reports for select packages by passing package paths:', + ); + console.log(''); + console.log( + ' yarn build:api-reports packages/config packages/core-plugin-api', + ); + console.log(''); + } + + let temporaryTsConfigPath: string | undefined; + if (selectedPackageDirs) { + temporaryTsConfigPath = await createTemporaryTsConfig(selectedPackageDirs); + } + const tsconfigFilePath = + temporaryTsConfigPath ?? resolvePath(projectRoot, 'tsconfig.json'); + + if (runTsc) { + await fs.remove(resolvePath(projectRoot, 'dist-types')); + const { status } = spawnSync( + 'yarn', + [ + 'tsc', + ['--project', tsconfigFilePath], + ['--skipLibCheck', 'false'], + ['--incremental', 'false'], + ].flat(), + { + stdio: 'inherit', + shell: true, + cwd: projectRoot, + }, + ); + if (status !== 0) { + process.exit(status || undefined); + } + } + + const packageDirs = selectedPackageDirs ?? (await findPackageDirs()); + + const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs( + projectRoot, + packageDirs, + ); + + if (tsPackageDirs.length > 0) { + console.log('# Generating package API reports'); + await runApiExtraction({ + packageDirs: tsPackageDirs, + outputDir: tmpDir, + isLocalBuild: !isCiBuild, + tsconfigFilePath, + }); + } + if (cliPackageDirs.length > 0) { + console.log('# Generating package CLI reports'); + await runCliExtraction({ + projectRoot, + packageDirs: cliPackageDirs, + isLocalBuild: !isCiBuild, + }); + } + + if (isDocsBuild) { + console.log('# Generating package documentation'); + await buildDocs({ + inputDir: tmpDir, + outputDir: resolvePath(projectRoot, 'docs/reference'), + }); + } +}; diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts new file mode 100644 index 0000000000..ebbcb5be8e --- /dev/null +++ b/packages/repo-tools/src/commands/index.ts @@ -0,0 +1,53 @@ +/* + * 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 { assertError } from '@backstage/errors'; +import { Command } from 'commander'; +import { exitWithError } from '../lib/errors'; + +export function registerCommands(program: Command) { + program + .command('api-reports [path...]') + .option('--ci', 'CI run checks that there is no changes on API reports') + .option('--tsc', 'executes the tsc compilation before extracting the APIs') + .option('--docs', 'generates the api documentation') + .description('Generate an API report for selected packages') + .action( + lazy(() => import('./api-reports/api-reports').then(m => m.default)), + ); + + program + .command('type-deps') + .description('Find inconsistencies in types of all packages and plugins') + .action(lazy(() => import('./type-deps/type-deps').then(m => m.default))); +} + +// Wraps an action function so that it always exits and handles errors +function lazy( + getActionFunc: () => Promise<(...args: any[]) => Promise>, +): (...args: any[]) => Promise { + return async (...args: any[]) => { + try { + const actionFunc = await getActionFunc(); + await actionFunc(...args); + + process.exit(0); + } catch (error) { + assertError(error); + exitWithError(error); + } + }; +} diff --git a/scripts/check-type-dependencies.js b/packages/repo-tools/src/commands/type-deps/type-deps.ts old mode 100755 new mode 100644 similarity index 79% rename from scripts/check-type-dependencies.js rename to packages/repo-tools/src/commands/type-deps/type-deps.ts index ba362bd5ff..d4dd28aa14 --- a/scripts/check-type-dependencies.js +++ b/packages/repo-tools/src/commands/type-deps/type-deps.ts @@ -1,6 +1,5 @@ -#!/usr/bin/env node /* - * Copyright 2020 The Backstage Authors + * 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. @@ -15,14 +14,14 @@ * limitations under the License. */ -const fs = require('fs'); -const { resolve: resolvePath } = require('path'); +import fs from 'fs'; +import { resolve as resolvePath } from 'path'; // Cba polluting root package.json, we'll have this // eslint-disable-next-line import/no-extraneous-dependencies -const chalk = require('chalk'); -const { getPackages } = require('@manypkg/get-packages'); +import chalk from 'chalk'; +import { getPackages, Package } from '@manypkg/get-packages'; -async function main() { +export default async () => { const { packages } = await getPackages(resolvePath('.')); let hadErrors = false; @@ -63,24 +62,29 @@ async function main() { process.exit(2); } -} +}; -function shouldCheckTypes(pkg) { +type PackageJsonWithTypes = { + packageJson: { + types?: string; + }; +}; +function shouldCheckTypes(pkg: Package & PackageJsonWithTypes) { return ( - !pkg.private && + !pkg.packageJson.private && pkg.packageJson.types && fs.existsSync(resolvePath(pkg.dir, 'dist/index.d.ts')) ); } -function findAllDeps(declSrc) { +function findAllDeps(declSrc: string) { const importedDeps = (declSrc.match(/^import .* from '.*';$/gm) || []) - .map(match => match.match(/from '(.*)'/)[1]) + .map(match => match.match(/from '(.*)'/)?.[1] ?? '') .filter(n => !n.startsWith('.')); const referencedDeps = ( declSrc.match(/^\/\/\/ $/gm) || [] ) - .map(match => match.match(/types="(.*)"/)[1]) + .map(match => match.match(/types="(.*)"/)?.[1] ?? '') .filter(n => !n.startsWith('.')) // We allow references to these without an explicit dependency. .filter(n => !['node', 'react'].includes(n)); @@ -91,7 +95,7 @@ function findAllDeps(declSrc) { * Scan index.d.ts for imports and return errors for any dependency that's * missing or incorrect in package.json */ -function checkTypes(pkg) { +function checkTypes(pkg: Package) { const typeDecl = fs.readFileSync( resolvePath(pkg.dir, 'dist/index.d.ts'), 'utf8', @@ -121,7 +125,7 @@ function checkTypes(pkg) { * Find the package used for types. This assumes that types are working is a package * can be resolved, it doesn't do any checking of presence of types inside the dep. */ -function findTypesPackage(dep, pkg) { +function findTypesPackage(dep: string, pkg: Package) { try { require.resolve(`@types/${dep}/package.json`, { paths: [pkg.dir] }); return `@types/${dep}`; @@ -136,10 +140,21 @@ function findTypesPackage(dep, pkg) { return undefined; } catch { try { - // Finally check if it's just a .d.ts file + // Check if it's just a .d.ts file require.resolve(`${dep}.d.ts`, { paths: [pkg.dir] }); return undefined; } catch { + // And finally a naive lookup of the file directly, in case `require.resolve` fails us due to "exports" + if (fs.existsSync(resolvePath(pkg.dir, `node_modules/${dep}.d.ts`))) { + return undefined; + } + if ( + fs.existsSync( + resolvePath(pkg.dir, `../../node_modules/${dep}.d.ts`), + ) + ) { + return undefined; + } throw mkErr('MissingDepError', `No types for ${dep}`, { dep }); } } @@ -150,7 +165,7 @@ function findTypesPackage(dep, pkg) { /** * Figures out what type dependencies are missing, or should be moved between dep types */ -function findTypeDepErrors(typeDeps, pkg) { +function findTypeDepErrors(typeDeps: string[], pkg: Package) { const devDeps = mkTypeDepSet(pkg.packageJson.devDependencies); const deps = mkTypeDepSet({ ...pkg.packageJson.dependencies, @@ -193,19 +208,14 @@ function findTypeDepErrors(typeDeps, pkg) { return errors; } -function mkTypeDepSet(deps) { +function mkTypeDepSet(deps: Record | undefined) { const typeDeps = Object.keys(deps || {}).filter(n => n.startsWith('@types/')); return new Set(typeDeps); } -function mkErr(name, msg, extra) { +function mkErr(name: string, msg: string, extra: Record) { const error = new Error(msg); error.name = name; Object.assign(error, extra); return error; } - -main().catch(error => { - console.error(error.stack || error); - process.exit(1); -}); diff --git a/packages/repo-tools/src/index.ts b/packages/repo-tools/src/index.ts new file mode 100644 index 0000000000..ed0e2992d3 --- /dev/null +++ b/packages/repo-tools/src/index.ts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +/** + * CLI for Backstage repo tooling + * + * @packageDocumentation + */ + +import { program } from 'commander'; +import chalk from 'chalk'; +import { exitWithError } from './lib/errors'; +import { registerCommands } from './commands'; + +const main = (argv: string[]) => { + program.name('backstage-repo-tools').version('1.0'); + + registerCommands(program); + + program.on('command:*', () => { + console.log(); + console.log(chalk.red(`Invalid command: ${program.args.join(' ')}`)); + console.log(); + program.outputHelp(); + process.exit(1); + }); + + program.parse(argv); +}; + +process.on('unhandledRejection', rejection => { + if (rejection instanceof Error) { + exitWithError(rejection); + } else { + exitWithError(new Error(`Unknown rejection: '${rejection}'`)); + } +}); + +main(process.argv); diff --git a/packages/repo-tools/src/lib/errors.ts b/packages/repo-tools/src/lib/errors.ts new file mode 100644 index 0000000000..dd3955a10a --- /dev/null +++ b/packages/repo-tools/src/lib/errors.ts @@ -0,0 +1,48 @@ +/* + * 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 chalk from 'chalk'; + +export class CustomError extends Error { + get name(): string { + return this.constructor.name; + } +} + +export class ExitCodeError extends CustomError { + readonly code: number; + + constructor(code: number, command?: string) { + super( + command + ? `Command '${command}' exited with code ${code}` + : `Child exited with code ${code}`, + ); + this.code = code; + } +} + +export function exitWithError(error: Error): never { + if (error instanceof ExitCodeError) { + process.stderr.write(`\n${chalk.red(error.message)}\n\n`); + process.exit(error.code); + } else { + process.stderr.write(`\n${chalk.red(`${error}`)}\n\n`); + process.exit(1); + } +} + +export class NotFoundError extends CustomError {} diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index fc7dbe743b..fde2899596 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,88 @@ # techdocs-cli-embedded-app +## 0.2.77-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-catalog@1.6.2-next.1 + - @backstage/plugin-techdocs@1.4.1-next.1 + - @backstage/test-utils@1.2.3-next.1 + - @backstage/cli@0.21.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/app-defaults@1.0.9-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.2.77-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/cli@0.21.2-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/test-utils@1.2.3-next.0 + - @backstage/plugin-techdocs@1.4.1-next.0 + - @backstage/plugin-catalog@1.6.2-next.0 + - @backstage/app-defaults@1.0.9-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## 0.2.76 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0 + - @backstage/core-components@0.12.0 + - @backstage/core-app-api@1.2.0 + - @backstage/cli@0.21.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/plugin-catalog@1.6.1 + - @backstage/app-defaults@1.0.8 + - @backstage/integration-react@1.1.6 + - @backstage/test-utils@1.2.2 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## 0.2.76-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0-next.2 + - @backstage/cli@0.21.0-next.1 + - @backstage/core-components@0.12.0-next.1 + - @backstage/app-defaults@1.0.8-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/test-utils@1.2.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog@1.6.1-next.1 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + +## 0.2.76-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0-next.1 + ## 0.2.76-next.0 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index c34c52e3f2..79aef2a74a 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.76-next.0", + "version": "0.2.77-next.1", "private": true, "backstage": { "role": "frontend" diff --git a/packages/techdocs-cli-embedded-app/public/index.html b/packages/techdocs-cli-embedded-app/public/index.html index b548d8fb70..c6083b3dd2 100644 --- a/packages/techdocs-cli-embedded-app/public/index.html +++ b/packages/techdocs-cli-embedded-app/public/index.html @@ -54,8 +54,8 @@ You can add webfonts, meta tags, or analytics to this file. The build step will place the bundled scripts into the tag. - To begin the development, run `npm start` or `yarn start`. - To create a production bundle, use `npm run build` or `yarn build`. + To begin the development, run `yarn start`. + To create a production bundle, use `yarn build`. --> diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 26f085b6ce..23aedd7f4f 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,49 @@ # @techdocs/cli +## 1.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-techdocs-node@1.4.3-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/cli-common@0.1.10 + +## 1.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-techdocs-node@1.4.3-next.0 + +## 1.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-techdocs-node@1.4.2 + - @backstage/catalog-model@1.1.3 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4 + +## 1.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-techdocs-node@1.4.2-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/cli-common@0.1.10 + - @backstage/config@1.0.4-next.0 + ## 1.2.3-next.0 ### Patch Changes diff --git a/packages/techdocs-cli/e2e-tests/techdocs-cli.test.ts b/packages/techdocs-cli/e2e-tests/techdocs-cli.test.ts index 15d420af2c..dbbc219ab6 100644 --- a/packages/techdocs-cli/e2e-tests/techdocs-cli.test.ts +++ b/packages/techdocs-cli/e2e-tests/techdocs-cli.test.ts @@ -64,7 +64,7 @@ describe('end-to-end', () => { afterEach(async () => { // On Windows the pid of a spawned process may be wrong - // Because of this, we should be kill the MKDocs after the test + // Because of this, we should stop the MKDocs after the test // (e.g. https://github.com/nodejs/node/issues/4289#issuecomment-854270414) if (process.platform === 'win32') { const procs = await findProcess('name', 'mkdocs', true); diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 10b738c025..7528a338c7 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.2.3-next.0", + "version": "1.2.4-next.1", "publishConfig": { "access": "public" }, diff --git a/packages/techdocs-cli/src/lib/run.ts b/packages/techdocs-cli/src/lib/run.ts index 9e2abeb617..13ac7d9d4c 100644 --- a/packages/techdocs-cli/src/lib/run.ts +++ b/packages/techdocs-cli/src/lib/run.ts @@ -63,7 +63,7 @@ export const run = async ( return childProcess; }; -// Block indefinitely and wait for a signal to kill the child process(es) +// Block indefinitely and wait for a signal to stop the child process(es) // Throw error if any child process errors // Resolves only when all processes exit with status code 0 export async function waitForSignal( diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index c2e2975656..7b1a7212fd 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/test-utils +## 1.2.3-next.1 + +### Patch Changes + +- 5e238ed56a: The test utility for the plugin context called `MockPluginProvider` has been created. It will be handy in the cases when you use + `__experimentalConfigure` in your plugin. It is experimental and exported through `@backstage/test-utils/alpha`. +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## 1.2.3-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.8-next.0 + +## 1.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.2.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.7 + ## 1.2.2-next.0 ### Patch Changes diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 38d6a85f5f..e11072f5a6 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -23,8 +23,10 @@ import { FetchApi } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; +import { MatcherFunction } from '@testing-library/react'; import { Observable } from '@backstage/types'; import { PermissionApi } from '@backstage/plugin-permission-react'; +import { PropsWithChildren } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RenderOptions } from '@testing-library/react'; @@ -165,6 +167,11 @@ export class MockPermissionApi implements PermissionApi { ): Promise; } +// @alpha +export const MockPluginProvider: ({ + children, +}: PropsWithChildren<{}>) => JSX.Element; + // @public export class MockStorageApi implements StorageApi { // (undocumented) @@ -235,6 +242,9 @@ export type TestAppOptions = { }; }; +// @public +export const textContentMatcher: (text: string) => MatcherFunction; + // @public export function withLogCollector( callback: AsyncLogCollector, diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 70e6dca4cc..0ee70ce834 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,11 +1,12 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "1.2.2-next.0", + "version": "1.2.3-next.1", "publishConfig": { "access": "public", "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "web-library" @@ -23,7 +24,7 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -45,7 +46,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "cross-fetch": "^3.1.5", - "zen-observable": "^0.8.15" + "zen-observable": "^0.10.0" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", @@ -57,9 +58,10 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/node": "^16.11.26", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 3d2b5a0212..a19fe53301 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -29,7 +29,7 @@ import { attachComponentData, createRouteRef, } from '@backstage/core-plugin-api'; -import { RenderResult } from '@testing-library/react'; +import { MatcherFunction, RenderResult } from '@testing-library/react'; import { renderWithEffects } from './testingLibrary'; import { defaultApis } from './defaultApis'; import { mockApis } from './mockApis'; @@ -242,3 +242,24 @@ export async function renderInTestApp( wrapper: createTestAppWrapper(options), }); } + +/** + * Returns a `@testing-library/react` valid MatcherFunction for supplied text + * + * @param string - text Text to match by element's textContent + * + * @public + */ +export const textContentMatcher = + (text: string): MatcherFunction => + (_, node) => { + if (!node) { + return false; + } + + const hasText = (textNode: Element) => textNode?.textContent === text; + const childrenDontHaveText = (containerNode: Element) => + Array.from(containerNode?.children).every(child => !hasText(child)); + + return hasText(node) && childrenDontHaveText(node); + }; diff --git a/packages/test-utils/src/testUtils/index.tsx b/packages/test-utils/src/testUtils/index.tsx index bfd870e2ad..8fc1395eb8 100644 --- a/packages/test-utils/src/testUtils/index.tsx +++ b/packages/test-utils/src/testUtils/index.tsx @@ -19,11 +19,13 @@ export { default as mockBreakpoint } from './mockBreakpoint'; export { wrapInTestApp, renderInTestApp, + textContentMatcher, createTestAppWrapper, } from './appWrappers'; export type { TestAppOptions } from './appWrappers'; export * from './msw'; export * from './logCollector'; +export * from './providers'; export * from './testingLibrary'; export { TestApiProvider, TestApiRegistry } from './TestApiProvider'; export type { TestApiProviderProps } from './TestApiProvider'; diff --git a/packages/test-utils/src/testUtils/providers.tsx b/packages/test-utils/src/testUtils/providers.tsx new file mode 100644 index 0000000000..8bc4b5122d --- /dev/null +++ b/packages/test-utils/src/testUtils/providers.tsx @@ -0,0 +1,35 @@ +/* + * 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 React, { PropsWithChildren } from 'react'; +import { createPlugin, PluginProvider } from '@backstage/core-plugin-api'; + +/** + * Mock for PluginProvider to use in unit tests + * @alpha + */ +export const MockPluginProvider = ({ children }: PropsWithChildren<{}>) => { + type TestInputPluginOptions = {}; + type TestPluginOptions = {}; + const plugin = createPlugin({ + id: 'my-plugin', + __experimentalConfigure(_: TestInputPluginOptions): TestPluginOptions { + return {}; + }, + }); + + return {children}; +}; diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index 9070442f8f..e0f1f56fa4 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/types +## 1.0.2-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. + +## 1.0.2-next.0 + +### Patch Changes + +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. + +## 1.0.1 + +### Patch Changes + +- 30e43717c7: Added the `HumanDuration` type, moved here from `@backstage/backend-tasks`. This type matches the `Duration.fromObject` form of `luxon`. + ## 1.0.1-next.0 ### Patch Changes diff --git a/packages/types/package.json b/packages/types/package.json index a9c340cec3..fba7a644df 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/types", "description": "Common TypeScript types used within Backstage", - "version": "1.0.1-next.0", + "version": "1.0.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "@backstage/cli": "workspace:^", "@types/zen-observable": "^0.8.0", "luxon": "^3.0.0", - "zen-observable": "^0.8.15" + "zen-observable": "^0.10.0" }, "files": [ "dist" diff --git a/packages/version-bridge/CHANGELOG.md b/packages/version-bridge/CHANGELOG.md index c3c3a9a57f..fd5ac8500d 100644 --- a/packages/version-bridge/CHANGELOG.md +++ b/packages/version-bridge/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/version-bridge +## 1.0.3-next.0 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals + +## 1.0.2 + +### Patch Changes + +- e70984325d: Updated the value created by `createVersionedValueMap` to give it an extra hidden property that makes it easier to inspect during debugging. + ## 1.0.1 ### Patch Changes diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index 3abfcd40f6..dbb4d69d4f 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/version-bridge", "description": "Utilities used by @backstage packages to support multiple concurrent versions", - "version": "1.0.1", + "version": "1.0.3-next.0", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/version-bridge/src/lib/VersionedValue.ts b/packages/version-bridge/src/lib/VersionedValue.ts index b7c2153e38..51a9fd7879 100644 --- a/packages/version-bridge/src/lib/VersionedValue.ts +++ b/packages/version-bridge/src/lib/VersionedValue.ts @@ -36,9 +36,17 @@ export function createVersionedValueMap< Versions extends { [version: number]: unknown }, >(versions: Versions): VersionedValue { Object.freeze(versions); - return { + const versionedValue: VersionedValue = { atVersion(version) { return versions[version]; }, }; + Object.defineProperty(versionedValue, '$map', { + configurable: true, + enumerable: true, + get() { + return versions; + }, + }); + return versionedValue; } diff --git a/packages/version-bridge/src/lib/globalObject.ts b/packages/version-bridge/src/lib/globalObject.ts index b29e81e4a0..19b6b73ccf 100644 --- a/packages/version-bridge/src/lib/globalObject.ts +++ b/packages/version-bridge/src/lib/globalObject.ts @@ -19,7 +19,9 @@ function getGlobalObject() { if (typeof window !== 'undefined' && window.Math === Math) { return window; } + // eslint-disable-next-line no-restricted-globals if (typeof self !== 'undefined' && self.Math === Math) { + // eslint-disable-next-line no-restricted-globals return self; } // eslint-disable-next-line no-new-func diff --git a/plugins/adr-backend/CHANGELOG.md b/plugins/adr-backend/CHANGELOG.md index 84143ccf78..9a4a722d4b 100644 --- a/plugins/adr-backend/CHANGELOG.md +++ b/plugins/adr-backend/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-adr-backend +## 0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-adr-common@0.2.4-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 0.2.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-adr-common@0.2.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-adr-common@0.2.3 + - @backstage/plugin-search-common@1.1.1 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/plugin-adr-common@0.2.3-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 0.2.3-next.0 ### Patch Changes diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index ccbd3f119e..5bd8b1434b 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr-backend", - "version": "0.2.3-next.0", + "version": "0.2.4-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -46,7 +46,7 @@ "@backstage/cli": "workspace:^", "@types/marked": "^4.0.0", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/adr-common/CHANGELOG.md b/plugins/adr-common/CHANGELOG.md index f9093a1dcf..b90cf4ffb1 100644 --- a/plugins/adr-common/CHANGELOG.md +++ b/plugins/adr-common/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-adr-common +## 0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.4.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-search-common@1.1.1 + ## 0.2.3-next.0 ### Patch Changes diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json index e9a9edc597..46602b837c 100644 --- a/plugins/adr-common/package.json +++ b/plugins/adr-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-adr-common", "description": "Common functionalities for the adr plugin", - "version": "0.2.3-next.0", + "version": "0.2.4-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md index e1f34381e4..8f989e969a 100644 --- a/plugins/adr/CHANGELOG.md +++ b/plugins/adr/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-adr +## 0.2.4-next.1 + +### Patch Changes + +- a19cffbeed: Update search links to only have header as linkable text +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.4-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 0.2.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-search-react@1.2.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.3 + - @backstage/plugin-search-common@1.1.1 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.3-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + ## 0.2.3-next.0 ### Patch Changes diff --git a/plugins/adr/package.json b/plugins/adr/package.json index e6113c7a83..311941a987 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr", - "version": "0.2.3-next.0", + "version": "0.2.4-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -55,7 +55,7 @@ "@types/git-url-parse": "^9.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/adr/src/search/AdrSearchResultListItem.tsx b/plugins/adr/src/search/AdrSearchResultListItem.tsx index d76f2cca4e..97847ba67b 100644 --- a/plugins/adr/src/search/AdrSearchResultListItem.tsx +++ b/plugins/adr/src/search/AdrSearchResultListItem.tsx @@ -64,21 +64,23 @@ export function AdrSearchResultListItem(props: { }; return ( - + <> - ) : ( - result.title - ) + + {highlight?.fields.title ? ( + + ) : ( + result.title + )} + } secondary={ - + ); } diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md index 38ed0acbc7..3c7a4e3aec 100644 --- a/plugins/airbrake-backend/CHANGELOG.md +++ b/plugins/airbrake-backend/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-airbrake-backend +## 0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + +## 0.2.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.2.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + +## 0.2.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + ## 0.2.11-next.0 ### Patch Changes diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json index f8904563a4..6ef89e40a6 100644 --- a/plugins/airbrake-backend/package.json +++ b/plugins/airbrake-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake-backend", - "version": "0.2.11-next.0", + "version": "0.2.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "@backstage/cli": "workspace:^", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index a714e4d69f..933c957ba6 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-airbrake +## 0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/test-utils@1.2.3-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/dev-utils@1.0.9-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.3.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/test-utils@1.2.3-next.0 + - @backstage/dev-utils@1.0.9-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.3.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/dev-utils@1.0.8 + - @backstage/test-utils@1.2.2 + - @backstage/theme@0.2.16 + +## 0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/dev-utils@1.0.8-next.1 + - @backstage/test-utils@1.2.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 3f04501019..8be500e662 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.3.11-next.0", + "version": "0.3.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -50,7 +50,7 @@ "@types/node": "^16.11.26", "@types/object-hash": "^2.2.1", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index 0f57d596c5..17264cdfd5 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-allure +## 0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.28-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.27 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.1.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.27-next.0 ### Patch Changes diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 68808cf511..e4f5b8a987 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-allure", "description": "A Backstage plugin that integrates with Allure", - "version": "0.1.27-next.0", + "version": "0.1.28-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -47,7 +47,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index 149215b223..315c8b6669 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-analytics-module-ga +## 0.1.23-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/theme@0.2.16 + +## 0.1.23-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## 0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## 0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + ## 0.1.22-next.0 ### Patch Changes diff --git a/plugins/analytics-module-ga/README.md b/plugins/analytics-module-ga/README.md index 4b822ea3e3..c2d79aa45d 100644 --- a/plugins/analytics-module-ga/README.md +++ b/plugins/analytics-module-ga/README.md @@ -169,6 +169,33 @@ export const apis: AnyApiFactory[] = [ ]; ``` +### Enabling Site Search + +If you wish to see all of the search events in the [Site Search](https://support.google.com/analytics/answer/1012264) +section of Google Analytics, you can enable sending virtual pageviews on every `search` event like so: + +```yaml +app: + analytics: + ga: + virtualSearchPageView: + mode: only # Defaults to 'disabled' + mountPath: /virtual-search # Defaults to '/search' + searchQuery: term # Defaults to 'query' + categoryQuery: sc # Omitted by default +``` + +Available `mode`s are: + +- `disabled` - no virtual pageviews are sent, default behavior +- `only` - sends virtual pageviews _instead_ of `search` events +- `both` - sends both virtual pageviews _and_ `search` events + +Virtual pageviews will be sent to the path specified in the `mountPath`, the search term will be +set as the value for query parameter `searchQuery` and category (if provided) will be set as the value for +query parameter `categoryQuery`, e.g. the example config above will result in +virtual pageviews being sent to `/virtual-search?term=SearchTermHere&sc=CategoryHere`. + ### Debugging and Testing In pre-production environments, you may wish to set additional configurations diff --git a/plugins/analytics-module-ga/config.d.ts b/plugins/analytics-module-ga/config.d.ts index b91f5812c1..188727bf93 100644 --- a/plugins/analytics-module-ga/config.d.ts +++ b/plugins/analytics-module-ga/config.d.ts @@ -53,6 +53,38 @@ export interface Config { */ identity?: 'disabled' | 'optional' | 'required'; + /** + * Controls whether to send virtual pageviews on `search` events. + * Can be used to enable Site Search in GA. + */ + virtualSearchPageView?: { + /** + * - `disabled`: (Default) no virtual pageviews are sent + * - `only`: Sends virtual pageview _instead_ of the `search` event + * - `both`: Sends both the `search` event _and_ the virtual pageview + * @visibility frontend + */ + mode?: 'disabled' | 'only' | 'both'; + /** + * Specifies on which path the main Search page is mounted. + * Defaults to `/search`. + * @visibility frontend + */ + mountPath?: string; + /** + * Specifies which query param is used for the term query in the virtual pageview URL. + * Defaults to `query`. + * @visibility frontend + */ + searchQuery?: string; + /** + * Specifies which query param is used for the category query in the virtual pageview URL. + * Skipped by default. + * @visibility frontend + */ + categoryQuery?: string; + }; + /** * Whether or not to log analytics debug statements to the console. * Defaults to false. diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index ac94ad8979..2ec2cbb542 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga", - "version": "0.1.22-next.0", + "version": "0.1.23-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -45,7 +45,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts index c9c2a02c69..65e9256aa8 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts @@ -25,6 +25,7 @@ describe('GoogleAnalytics', () => { pluginId: 'some-plugin', routeRef: 'unknown', releaseNum: 1337, + searchTypes: 'test category', }; const trackingId = 'UA-000000-0'; const basicValidConfig = new ConfigReader({ @@ -152,6 +153,101 @@ describe('GoogleAnalytics', () => { }); }); + it('captures virtual pageviews instead of search events', () => { + const config = new ConfigReader({ + app: { + analytics: { + ga: { + trackingId, + testMode: true, + virtualSearchPageView: { mode: 'only' }, + }, + }, + }, + }); + const api = GoogleAnalytics.fromConfig(config); + api.captureEvent({ + action: 'search', + subject: 'test search', + context, + }); + + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'pageview', + page: '/search?query=test+search', + }); + expect(ReactGA.testModeAPI.calls).toHaveLength(2); + }); + + it('captures virtual pageviews alongside search events', () => { + const config = new ConfigReader({ + app: { + analytics: { + ga: { + trackingId, + testMode: true, + virtualSearchPageView: { mode: 'both' }, + }, + }, + }, + }); + const api = GoogleAnalytics.fromConfig(config); + api.captureEvent({ + action: 'search', + subject: 'test search', + context, + }); + + const [pageviewCommand, pageViewData] = ReactGA.testModeAPI.calls[1]; + expect(pageviewCommand).toBe('send'); + expect(pageViewData).toMatchObject({ + hitType: 'pageview', + page: '/search?query=test+search', + }); + const [searchCommand, searchData] = ReactGA.testModeAPI.calls[2]; + expect(searchCommand).toBe('send'); + expect(searchData).toMatchObject({ + hitType: 'event', + eventCategory: context.extension, + eventAction: 'search', + eventLabel: 'test search', + }); + }); + + it('captures virtual pageviews on custom route with custom search query and custom category', () => { + const config = new ConfigReader({ + app: { + analytics: { + ga: { + trackingId, + testMode: true, + virtualSearchPageView: { + mode: 'only', + mountPath: '/custom', + searchQuery: 'term', + categoryQuery: 'sc', + }, + }, + }, + }, + }); + const api = GoogleAnalytics.fromConfig(config); + api.captureEvent({ + action: 'search', + subject: 'test search', + context, + }); + + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'pageview', + page: '/custom?term=test+search&sc=test+category', + }); + }); + it('captures configured custom dimensions/metrics on events', () => { const api = GoogleAnalytics.fromConfig(advancedConfig); diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts index ad7205983c..17c5b26be1 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts @@ -18,12 +18,16 @@ import ReactGA from 'react-ga'; import { AnalyticsApi, AnalyticsContextValue, - AnalyticsEventAttributes, AnalyticsEvent, + AnalyticsEventAttributes, IdentityApi, } from '@backstage/core-plugin-api'; import { Config } from '@backstage/config'; import { DeferredCapture } from '../../../util'; +import { + parseVirtualSearchPageViewConfig, + VirtualSearchPageViewConfig, +} from '../../../util/VirtualSearchPageView'; type CustomDimensionOrMetricConfig = { type: 'dimension' | 'metric'; @@ -40,6 +44,7 @@ export class GoogleAnalytics implements AnalyticsApi { private readonly cdmConfig: CustomDimensionOrMetricConfig[]; private customUserIdTransform?: (userEntityRef: string) => Promise; private readonly capture: DeferredCapture; + private readonly virtualSearchPageView: VirtualSearchPageViewConfig; /** * Instantiate the implementation and initialize ReactGA. @@ -51,6 +56,7 @@ export class GoogleAnalytics implements AnalyticsApi { identity: string; trackingId: string; scriptSrc?: string; + virtualSearchPageView: VirtualSearchPageViewConfig; testMode: boolean; debug: boolean; }) { @@ -61,11 +67,13 @@ export class GoogleAnalytics implements AnalyticsApi { identityApi, userIdTransform = 'sha-256', scriptSrc, + virtualSearchPageView, testMode, debug, } = options; this.cdmConfig = cdmConfig; + this.virtualSearchPageView = virtualSearchPageView; // Initialize Google Analytics. ReactGA.initialize(trackingId, { @@ -105,6 +113,9 @@ export class GoogleAnalytics implements AnalyticsApi { const scriptSrc = config.getOptionalString('app.analytics.ga.scriptSrc'); const identity = config.getOptionalString('app.analytics.ga.identity') || 'disabled'; + const virtualSearchPageView = parseVirtualSearchPageViewConfig( + config.getOptionalConfig('app.analytics.ga.virtualSearchPageView'), + ); const debug = config.getOptionalBoolean('app.analytics.ga.debug') ?? false; const testMode = config.getOptionalBoolean('app.analytics.ga.testMode') ?? false; @@ -134,6 +145,7 @@ export class GoogleAnalytics implements AnalyticsApi { identity, trackingId, scriptSrc, + virtualSearchPageView, cdmConfig, testMode, debug, @@ -154,6 +166,23 @@ export class GoogleAnalytics implements AnalyticsApi { return; } + if (this.virtualSearchPageView.mode !== 'disabled' && action === 'search') { + const { mountPath, searchQuery, categoryQuery } = + this.virtualSearchPageView; + const params = new URLSearchParams(); + params.set(searchQuery, subject); + if (categoryQuery) { + params.set(categoryQuery, context.searchTypes?.toString() ?? ''); + } + this.capture.pageview( + `${mountPath}?${params.toString()}`, + customMetadata, + ); + if (this.virtualSearchPageView.mode === 'only') { + return; + } + } + this.capture.event({ category: context.extension || 'App', action, @@ -241,7 +270,7 @@ export class GoogleAnalytics implements AnalyticsApi { * Simple hash function; relies on web cryptography + the sha-256 algorithm. */ private async hash(value: string): Promise { - const digest = await crypto.subtle.digest( + const digest = await window.crypto.subtle.digest( 'sha-256', new TextEncoder().encode(value), ); diff --git a/plugins/analytics-module-ga/src/util/VirtualSearchPageView.ts b/plugins/analytics-module-ga/src/util/VirtualSearchPageView.ts new file mode 100644 index 0000000000..bb3d40afd3 --- /dev/null +++ b/plugins/analytics-module-ga/src/util/VirtualSearchPageView.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Config } from '@backstage/config'; + +type VirtualSearchPageViewType = 'disabled' | 'only' | 'both'; + +export type VirtualSearchPageViewConfig = { + mode: VirtualSearchPageViewType; + mountPath: string; + searchQuery: string; + categoryQuery?: string; +}; + +function isVirtualSearchPageViewType( + value: string | undefined, +): value is VirtualSearchPageViewType { + return value === 'disabled' || value === 'only' || value === 'both'; +} + +export function parseVirtualSearchPageViewConfig( + config: Config | undefined, +): VirtualSearchPageViewConfig { + const vspvModeString = config?.getOptionalString('mode'); + return { + mode: isVirtualSearchPageViewType(vspvModeString) + ? vspvModeString + : 'disabled', + mountPath: config?.getOptionalString('mountPath') ?? '/search', + searchQuery: config?.getOptionalString('searchQuery') ?? 'query', + categoryQuery: config?.getOptionalString('categoryQuery'), + }; +} diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index b4dee086eb..d4784b0b95 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-apache-airflow +## 0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + +## 0.2.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + +## 0.2.4 + +### Patch Changes + +- 989731cabf: 1. Added a new column in the table to quickly view the latest DAG runs, plus a link to it if you want to have a deeper look. 2. Table columns are togglable 3. Set hidden columns 4. Fixed bug with turning on/off the DAGs +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + +## 0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + ## 0.2.4-next.0 ### Patch Changes diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index bba1177b06..a3a0f2f885 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.2.4-next.0", + "version": "0.2.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,7 +43,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/apache-airflow/src/api/ApacheAirflowApi.ts b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts index 838292524a..128f6e551d 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowApi.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts @@ -16,6 +16,7 @@ import { createApiRef, DiscoveryApi } from '@backstage/core-plugin-api'; import { Dag, InstanceStatus, InstanceVersion } from './types'; +import { DagRun } from './types/Dags'; export const apacheAirflowApiRef = createApiRef({ id: 'plugin.apacheairflow.service', @@ -27,6 +28,10 @@ export type ApacheAirflowApi = { listDags(options?: { objectsPerRequest: number }): Promise; getDags(dagIds: string[]): Promise<{ dags: Dag[]; dagsNotFound: string[] }>; updateDag(dagId: string, isPaused: boolean): Promise; + getDagRuns( + dagId: string, + options?: { objectsPerRequest?: number; limit?: number }, + ): Promise; getInstanceStatus(): Promise; getInstanceVersion(): Promise; }; diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts index b9bdb02283..ab0565f4e4 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts @@ -20,6 +20,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ApacheAirflowClient } from './index'; import { Dag } from './types'; +import { DagRun } from './types/Dags'; const server = setupServer(); @@ -66,12 +67,45 @@ const dags: Dag[] = [ }, ]; +const dagRuns: DagRun[] = [ + { + dag_run_id: 'mock dag run 1', + dag_id: 'mock_dag_1', + logical_date: '2022-05-27T11:25:23.251274+00:00', + start_date: '2022-05-27T11:25:23.251274+00:00', + end_date: '2022-05-27T11:25:23.251274+00:00', + state: 'success', + external_trigger: true, + conf: {}, + }, + { + dag_run_id: 'mock dag run 2', + dag_id: 'mock_dag_2', + logical_date: '2022-05-27T11:25:23.251274+00:00', + start_date: '2022-05-27T11:25:23.251274+00:00', + end_date: '2022-05-27T11:25:23.251274+00:00', + state: 'running', + external_trigger: true, + conf: {}, + }, + { + dag_run_id: 'mock dag run 3', + dag_id: 'mock_dag_1', + logical_date: '2022-05-27T11:25:23.251274+00:00', + start_date: '2022-05-27T11:25:23.251274+00:00', + end_date: '2022-05-27T11:25:23.251274+00:00', + state: 'failed', + external_trigger: true, + conf: {}, + }, +]; + describe('ApacheAirflowClient', () => { setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191/api/proxy'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - + let client: ApacheAirflowClient; const setupHandlers = () => { server.use( rest.get(`${mockBaseUrl}/airflow/dags`, (req, res, ctx) => { @@ -116,41 +150,61 @@ describe('ApacheAirflowClient', () => { return res(ctx.status(404)); }), - rest.patch(`${mockBaseUrl}/airflow/dags/:dag_id`, (req, res, ctx) => { - const { dag_id } = req.params; - const body = JSON.parse(req.body as string); - expect(body.is_paused).toBeDefined(); - return res( - ctx.json({ - dag_id: dag_id, - root_dag_id: 'string', - is_paused: body.is_paused, - is_active: true, - is_subdag: true, - fileloc: 'string', - file_token: 'string', - owners: ['string'], - description: 'string', - schedule_interval: { - __type: 'string', - days: 0, - seconds: 0, - microseconds: 0, - }, - tags: [{}], - }), - ); - }), + rest.get( + `${mockBaseUrl}/airflow/dags/:dag_id/dagRuns`, + (req, res, ctx) => { + const { dag_id } = req.params; + const runs = dagRuns.filter(run => run.dag_id === dag_id); + // event if the dag_id is invalid, airflow returns a valid response (with 0 dag runs) + return res( + ctx.json({ + dag_runs: runs, + total_entries: runs.length, + }), + ); + }, + ), + + rest.patch( + `${mockBaseUrl}/airflow/dags/:dag_id`, + async (req, res, ctx) => { + const { dag_id } = req.params; + const body = JSON.parse(await req.text()); + expect(body.is_paused).toBeDefined(); + return res( + ctx.json({ + dag_id: dag_id, + root_dag_id: 'string', + is_paused: body.is_paused, + is_active: true, + is_subdag: true, + fileloc: 'string', + file_token: 'string', + owners: ['string'], + description: 'string', + schedule_interval: { + __type: 'string', + days: 0, + seconds: 0, + microseconds: 0, + }, + tags: [{}], + }), + ); + }, + ), ); }; - it('list dags should return all dags with emulated pagination', async () => { + beforeEach(() => { setupHandlers(); - const client = new ApacheAirflowClient({ + client = new ApacheAirflowClient({ discoveryApi: discoveryApi, baseUrl: 'localhost:8080/', }); + }); + it('list dags should return all dags with emulated pagination', async () => { // call with limit of 2, to force two paginations in requesting all dags // as our mocked response has 4 total entries const responseDags = await client.listDags({ objectsPerRequest: 2 }); @@ -159,11 +213,6 @@ describe('ApacheAirflowClient', () => { }); it('update dag should return dag information with updated paused attribute', async () => { - setupHandlers(); - const client = new ApacheAirflowClient({ - discoveryApi: discoveryApi, - baseUrl: 'localhost:8080/', - }); const dagId = 'mock_dag_1'; const response: Dag = await client.updateDag(dagId, true); expect(response.dag_id).toEqual(dagId); @@ -171,11 +220,6 @@ describe('ApacheAirflowClient', () => { }); it('get only some dags', async () => { - setupHandlers(); - const client = new ApacheAirflowClient({ - discoveryApi: discoveryApi, - baseUrl: 'localhost:8080/', - }); const dagIds = ['mock_dag_1', 'mock_dag_3']; const response = await client.getDags(dagIds); expect(response.dags.length).toEqual(dagIds.length); @@ -186,11 +230,6 @@ describe('ApacheAirflowClient', () => { }); it('get dags but ignore NOT FOUND errors', async () => { - setupHandlers(); - const client = new ApacheAirflowClient({ - discoveryApi: discoveryApi, - baseUrl: 'localhost:8080/', - }); const dagIds = ['mock_dag_1', 'a-random-DAG-id']; const response = await client.getDags(dagIds); expect(response.dags.length).toEqual(1); @@ -198,4 +237,11 @@ describe('ApacheAirflowClient', () => { expect(response.dagsNotFound.length).toEqual(1); expect(response.dagsNotFound[0]).toEqual('a-random-DAG-id'); }); + + it('should get dag runs', async () => { + const dagId = 'mock_dag_1'; + const response = await client.getDagRuns(dagId); + expect(response.length).toEqual(2); + response.forEach(run => expect(run.dag_id).toEqual(dagId)); + }); }); diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts index c722f5977d..ebd60b668a 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts @@ -25,6 +25,7 @@ import { InstanceVersion, ListDagsParams, } from './types'; +import { DagRun } from './types/Dags'; export class ApacheAirflowClient implements ApacheAirflowApi { discoveryApi: DiscoveryApi; @@ -97,11 +98,56 @@ export class ApacheAirflowClient implements ApacheAirflowApi { async updateDag(dagId: string, isPaused: boolean): Promise { const init = { method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify({ is_paused: isPaused }), }; return await this.fetch(`/dags/${dagId}`, init); } + /** + * Get the latest DAG Runs of a specific DAG. + * The DAG runs are ordered by start date + * + * @remarks + * + * The "limit" option means the maximum number of DAG runs to return + * "objectsPerRequest" means the maximum number of DAG runs to be taken per fetch request. + * This is just to make sure the response payloads are not too big + */ + async getDagRuns( + dagId: string, + options = { objectsPerRequest: 100, limit: 5 }, + ): Promise { + const dagRuns: DagRun[] = []; + const searchParams: ListDagsParams = { + limit: Math.min(options.objectsPerRequest || 100, options.limit || 5), + offset: 0, + order_by: '-start_date', + }; + + for (;;) { + const response = await this.fetch<{ + dag_runs: DagRun[]; + total_entries: number; + }>(`/dags/${dagId}/dagRuns?${qs.stringify(searchParams)}`); + dagRuns.push(...response.dag_runs); + + if (response.dag_runs.length < searchParams.limit!) { + break; + } + if ( + dagRuns.length >= response.total_entries || + dagRuns.length >= options.limit + ) { + break; + } + searchParams.offset! += response.dag_runs.length; + } + return dagRuns; + } + async getInstanceStatus(): Promise { return await this.fetch('/health'); } diff --git a/plugins/apache-airflow/src/api/types/Dags.ts b/plugins/apache-airflow/src/api/types/Dags.ts index 8f1d68fb2d..702a6470e7 100644 --- a/plugins/apache-airflow/src/api/types/Dags.ts +++ b/plugins/apache-airflow/src/api/types/Dags.ts @@ -44,6 +44,17 @@ export interface Dag { tags: Tag[]; } +export interface DagRun { + dag_run_id: string; + dag_id: string; + logical_date: string; + start_date: string; + end_date: string; + state: 'queued' | 'running' | 'success' | 'failed'; + external_trigger: boolean; + conf: {}; +} + export interface TimeDelta { __type: 'TimeDelta'; days: number; diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx index fcd87b2020..2660e9380e 100644 --- a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx @@ -22,7 +22,7 @@ import { TableColumn, WarningPanel, } from '@backstage/core-components'; -import { useApi } from '@backstage/core-plugin-api'; +import { storageApiRef, useApi } from '@backstage/core-plugin-api'; import Box from '@material-ui/core/Box'; import Chip from '@material-ui/core/Chip'; import IconButton from '@material-ui/core/IconButton'; @@ -31,101 +31,151 @@ import Tooltip from '@material-ui/core/Tooltip'; import Typography from '@material-ui/core/Typography'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; import Alert from '@material-ui/lab/Alert'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { apacheAirflowApiRef } from '../../api'; import { Dag } from '../../api/types'; import { ScheduleIntervalLabel } from '../ScheduleIntervalLabel'; +import { LatestDagRunsStatus } from '../LatestDagRunsStatus'; type DagTableRow = Dag & { id: string; dagUrl: string; }; -const columns: TableColumn[] = [ - { - title: 'Paused', - field: 'is_paused', - render: (row: Partial) => ( - - - - ), - width: '5%', - }, - { - title: 'DAG', - field: 'id', - render: (row: Partial) => ( -
- - {row.id} - - - {row.tags?.map((tag, ix) => ( - - ))} - -
- ), - width: '60%', - }, - { - title: 'Owner', - field: 'owners', - render: (row: Partial) => ( - - {row.owners?.map((owner, ix) => ( - - ))} - - ), - width: '10%', - }, - { - title: 'Active', - render: (row: Partial) => ( - - {row.is_active ? : } - - {row.is_active ? 'Active' : 'Inactive'} - - - ), - width: '10%', - }, - { - title: 'Schedule', - render: (row: Partial) => ( - - ), - width: '10%', - }, - { - title: 'Link', - field: 'dagUrl', - render: (row: Partial) => ( - - - - - - ), - width: '5%', - }, -]; - type DenseTableProps = { dags: Dag[]; + rowClick: Function; }; -export const DenseTable = ({ dags }: DenseTableProps) => { +export const DenseTable = ({ dags, rowClick }: DenseTableProps) => { + const storage = useApi(storageApiRef); + const hiddenColumnsKey = 'dag-table-hidden-columns'; + const [hiddenColumns, setHiddenColumns] = useState([]); + + useEffect(() => { + const hiddenState = storage.snapshot(hiddenColumnsKey); + if (hiddenState.presence === 'present') { + setHiddenColumns(hiddenState.value as string[]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const columns: TableColumn[] = [ + { + title: 'Paused', + field: 'is_paused', + render: (row: Partial) => ( + + + + ), + width: '5%', + hidden: hiddenColumns.some(field => field === 'is_paused'), + }, + { + title: 'DAG', + field: 'id', + render: (row: Partial) => ( +
+ + {row.id} + + + {row.tags?.map((tag, ix) => ( + + ))} + +
+ ), + width: '50%', + disableClick: true, + hidden: hiddenColumns.some(field => field === 'id'), + }, + { + title: 'Runs', + field: 'runs', + render: (row: Partial) => ( + + ), + width: '10%', + disableClick: true, + hidden: hiddenColumns.some(field => field === 'runs'), + }, + { + title: 'Owner', + field: 'owners', + render: (row: Partial) => ( + + {row.owners?.map((owner, ix) => ( + + ))} + + ), + width: '10%', + disableClick: true, + hidden: hiddenColumns.some(field => field === 'owners'), + }, + { + title: 'Active', + field: 'active', + render: (row: Partial) => ( + + {row.is_active ? : } + + {row.is_active ? 'Active' : 'Inactive'} + + + ), + width: '10%', + disableClick: true, + hidden: hiddenColumns.some(field => field === 'active'), + }, + { + title: 'Schedule', + field: 'schedule', + render: (row: Partial) => ( + + ), + width: '10%', + disableClick: true, + hidden: hiddenColumns.some(field => field === 'schedule'), + }, + { + title: 'Link', + field: 'dagUrl', + render: (row: Partial) => ( + + + + + + ), + width: '5%', + disableClick: true, + hidden: hiddenColumns.some(field => field === 'dagUrl'), + }, + ]; + return ( rowClick(rowData)} + onChangeColumnHidden={(column, hidden) => { + if (column.field) { + let newHiddenColumns: string[]; + if (hidden) { + newHiddenColumns = hiddenColumns.concat(column.field); + } else { + newHiddenColumns = hiddenColumns.filter(v => v !== column.field); + } + setHiddenColumns(newHiddenColumns); + storage.set(hiddenColumnsKey, newHiddenColumns); + } + }} /> ); }; @@ -136,27 +186,54 @@ type DagTableComponentProps = { export const DagTableComponent = (props: DagTableComponentProps) => { const { dagIds } = props; + const [dagsData, setDagsData] = useState([]); const apiClient = useApi(apacheAirflowApiRef); - const { value, loading, error } = useAsync(async (): Promise => { + const updatePaused = async (rowData: Dag): Promise => { + const newDag = await apiClient.updateDag( + rowData.dag_id, + !rowData.is_paused, + ); + + const newDags = dagsData.map(el => { + if (el.dag_id === newDag.dag_id) { + return { ...el, is_paused: newDag.is_paused }; + } + return el; + }); + + setDagsData(newDags); + return newDag; + }; + + const { value, loading, error } = useAsync(async (): Promise< + DagTableRow[] + > => { + let dags: Dag[] = []; if (dagIds) { - const { dags } = await apiClient.getDags(dagIds); - return dags; + dags = (await apiClient.getDags(dagIds)).dags; + } else { + dags = await apiClient.listDags(); } - return await apiClient.listDags(); + return dags.map(el => ({ + ...el, + id: el.dag_id, // table records require `id` attribute + dagUrl: `${apiClient.baseUrl}dag_details?dag_id=${el.dag_id}`, // construct path to DAG using `baseUrl` + })); }, []); + useEffect(() => { + if (value) { + setDagsData(value); + } + }, [value]); + if (loading) { return ; } else if (error) { return {error.message}; } - const data = value?.map(el => ({ - ...el, - id: el.dag_id, // table records require `id` attribute - dagUrl: `${apiClient.baseUrl}dag_details?dag_id=${el.dag_id}`, // construct path to DAG using `baseUrl` - })); const dagsNotFound = dagIds && value ? dagIds.filter(id => !value.find(d => d.dag_id === id)) @@ -172,7 +249,7 @@ export const DagTableComponent = (props: DagTableComponentProps) => { ) : ( '' )} - + ); }; diff --git a/plugins/apache-airflow/src/components/LatestDagRunsStatus/LatestDagRunsStatus.test.tsx b/plugins/apache-airflow/src/components/LatestDagRunsStatus/LatestDagRunsStatus.test.tsx new file mode 100644 index 0000000000..fb020ee9e9 --- /dev/null +++ b/plugins/apache-airflow/src/components/LatestDagRunsStatus/LatestDagRunsStatus.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ApacheAirflowApi, apacheAirflowApiRef } from '../../api'; +import { DagRun } from '../../api/types/Dags'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { LatestDagRunsStatus } from './LatestDagRunsStatus'; + +describe('LatestDagRunsStatus', () => { + const baseDagRun: Partial = { + dag_run_id: 'mock dag run 1', + dag_id: 'mock_dag_1', + logical_date: '2022-05-27T11:25:23.251274+00:00', + start_date: '2022-05-27T11:25:23.251274+00:00', + end_date: '2022-05-27T11:25:23.251274+00:00', + state: 'success', + external_trigger: true, + conf: {}, + }; + const mockApi: jest.Mocked = { + getDagRuns: jest.fn().mockResolvedValue([ + baseDagRun, + { + ...baseDagRun, + dag_run_id: 'mock dag run 2', + state: 'running', + }, + { + ...baseDagRun, + dag_run_id: 'mock dag run 3', + state: 'failed', + }, + { + ...baseDagRun, + dag_run_id: 'mock dag run 3', + state: 'queued', + }, + ] as DagRun[]), + } as any; + + it('should render the status of mock dag 1', async () => { + const dagId = 'mock_dag_1'; + + const { getByLabelText } = await renderInTestApp( + + + , + ); + expect(getByLabelText('Status ok')).toBeInTheDocument(); + expect(getByLabelText('Status running')).toBeInTheDocument(); + expect(getByLabelText('Status error')).toBeInTheDocument(); + expect(getByLabelText('Status pending')).toBeInTheDocument(); + }); +}); diff --git a/plugins/apache-airflow/src/components/LatestDagRunsStatus/LatestDagRunsStatus.tsx b/plugins/apache-airflow/src/components/LatestDagRunsStatus/LatestDagRunsStatus.tsx new file mode 100644 index 0000000000..90cb0694f8 --- /dev/null +++ b/plugins/apache-airflow/src/components/LatestDagRunsStatus/LatestDagRunsStatus.tsx @@ -0,0 +1,161 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { apacheAirflowApiRef } from '../../api'; +import useAsync from 'react-use/lib/useAsync'; +import { DagRun } from '../../api/types/Dags'; +import { useApi } from '@backstage/core-plugin-api'; +import { + Box, + Button, + CircularProgress, + List, + ListItem, + ListItemIcon, + makeStyles, + Tooltip, + Typography, +} from '@material-ui/core'; +import { + Link, + StatusError, + StatusOK, + StatusPending, + StatusRunning, +} from '@backstage/core-components'; +import DirectionsRun from '@material-ui/icons/DirectionsRun'; +import Check from '@material-ui/icons/Check'; +import CalendarToday from '@material-ui/icons/CalendarToday'; +import qs from 'qs'; +import AccountTree from '@material-ui/icons/AccountTree'; + +interface LatestDagRunsStatusProps { + dagId: string; + limit?: number; +} + +const useStyles = makeStyles(() => ({ + noMaxWidth: { + maxWidth: 'none', + }, +})); + +const DagRunTooltip = ({ + dagRun, + graphUrl, +}: { + dagRun: DagRun; + graphUrl: string; +}) => { + return ( + + + + + + {dagRun.dag_run_id} + + + + + + {new Date(dagRun.start_date).toLocaleString()} + + + + + + + {dagRun.end_date ? new Date(dagRun.end_date).toLocaleString() : '-'} + + + + + + + ); +}; + +export const LatestDagRunsStatus = ({ + dagId, + limit = 5, +}: LatestDagRunsStatusProps) => { + const classes = useStyles(); + const apiClient = useApi(apacheAirflowApiRef); + const { value, loading, error } = useAsync( + async (): Promise => await apiClient.getDagRuns(dagId, { limit }), + [dagId, limit], + ); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return Can't get dag runs; + } + + const statusDots: JSX.Element[] | undefined = value?.map(dagRun => { + function status() { + switch (dagRun.state) { + case 'success': + return ; + case 'failed': + return ; + case 'running': + return ; + case 'queued': + return ; + default: + return Unrecognized state; + } + } + + const key = dagRun.dag_id + dagRun.dag_run_id; + const dagRunParams = { + dag_id: dagRun.dag_id, + execution_date: dagRun.logical_date, + }; + const graphUrl = `${apiClient.baseUrl}graph?${qs.stringify(dagRunParams)}`; + return ( + } + key={key} + classes={{ tooltip: classes.noMaxWidth }} + interactive + > + + {status()} + + + ); + }); + + return {statusDots}; +}; diff --git a/plugins/apache-airflow/src/components/LatestDagRunsStatus/index.ts b/plugins/apache-airflow/src/components/LatestDagRunsStatus/index.ts new file mode 100644 index 0000000000..acd84a3584 --- /dev/null +++ b/plugins/apache-airflow/src/components/LatestDagRunsStatus/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { LatestDagRunsStatus } from './LatestDagRunsStatus'; diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index de93784591..7cba1b5f69 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-api-docs +## 0.8.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-catalog@1.6.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.8.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/plugin-catalog@1.6.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.8.11 + +### Patch Changes + +- 4db78c2296: Updated dependency `@asyncapi/react-component` to `1.0.0-next.44`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-catalog@1.6.1 + - @backstage/theme@0.2.16 + +## 0.8.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog@1.6.1-next.1 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.8.11-next.0 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index ecb6fe3ff4..2ebad8557d 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.8.11-next.0", + "version": "0.8.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@asyncapi/react-component": "1.0.0-next.43", + "@asyncapi/react-component": "1.0.0-next.44", "@backstage/catalog-model": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", @@ -66,7 +66,7 @@ "@types/node": "^16.11.26", "@types/swagger-ui-react": "^4.1.1", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md index 5ef5cb16f9..bfe055b253 100644 --- a/plugins/apollo-explorer/CHANGELOG.md +++ b/plugins/apollo-explorer/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-apollo-explorer +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## 0.1.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json index 91fe56950e..b5780aacc4 100644 --- a/plugins/apollo-explorer/package.json +++ b/plugins/apollo-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apollo-explorer", - "version": "0.1.4-next.0", + "version": "0.1.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -45,7 +45,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 81c2020152..0398ae4e93 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-app-backend +## 0.3.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config-loader@1.1.7-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + +## 0.3.39-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config-loader@1.1.7-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.3.38 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/config@1.0.4 + - @backstage/config-loader@1.1.6 + +## 0.3.38-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/config-loader@1.1.6-next.0 + - @backstage/types@1.0.1-next.0 + ## 0.3.38-next.0 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 5e72d7b666..cf438b5422 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.38-next.0", + "version": "0.3.39-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -58,7 +58,7 @@ "@types/supertest": "^2.0.8", "get-port": "^6.1.2", "mock-fs": "^5.1.0", - "msw": "^0.47.0", + "msw": "^0.49.0", "node-fetch": "^2.6.7", "supertest": "^6.1.3" }, diff --git a/plugins/app-backend/src/service/appPlugin.test.ts b/plugins/app-backend/src/service/appPlugin.test.ts index f4a2b142e8..8583726287 100644 --- a/plugins/app-backend/src/service/appPlugin.test.ts +++ b/plugins/app-backend/src/service/appPlugin.test.ts @@ -17,7 +17,7 @@ import mockFs from 'mock-fs'; import { resolve as resolvePath } from 'path'; import fetch from 'node-fetch'; -import { configServiceRef } from '@backstage/backend-plugin-api'; +import { coreServices } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; import { appPlugin } from './appPlugin'; import { @@ -51,7 +51,7 @@ describe('appPlugin', () => { await startTestBackend({ services: [ [ - configServiceRef, + coreServices.config, new ConfigReader({ backend: { listen: { port }, diff --git a/plugins/app-backend/src/service/appPlugin.ts b/plugins/app-backend/src/service/appPlugin.ts index f88bcb14f1..bfccf3cc02 100644 --- a/plugins/app-backend/src/service/appPlugin.ts +++ b/plugins/app-backend/src/service/appPlugin.ts @@ -16,12 +16,9 @@ import express from 'express'; import { - configServiceRef, + coreServices, createBackendPlugin, - databaseServiceRef, - loggerServiceRef, loggerToWinstonLogger, - httpRouterServiceRef, } from '@backstage/backend-plugin-api'; import { createRouter } from './router'; @@ -78,10 +75,10 @@ export const appPlugin = createBackendPlugin({ register(env, options: AppPluginOptions) { env.registerInit({ deps: { - logger: loggerServiceRef, - config: configServiceRef, - database: databaseServiceRef, - httpRouter: httpRouterServiceRef, + logger: coreServices.logger, + config: coreServices.config, + database: coreServices.database, + httpRouter: coreServices.httpRouter, }, async init({ logger, config, database, httpRouter }) { const { diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index a65fa417d8..8e23fa4d91 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-auth-backend +## 0.17.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.17.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.17.1 + +### Patch Changes + +- 0d6837ca4e: Fix wrong GitHub callback URL documentation +- cbe11d1e23: Tweak README +- 89d705e806: Add support for custom JWT header name in GCP IAP auth. +- abaed9770e: Improve logging +- d80833fe0c: Inject optional `CatalogApi` into auth-backend `createRouter` function. This will enable developers to use customized `CatalogApi` when creating the router. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/types@1.0.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.17.1-next.1 + +### Patch Changes + +- 0d6837ca4e: Fix wrong GitHub callback URL documentation +- abaed9770e: Improve logging +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + ## 0.17.1-next.0 ### Patch Changes diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index d13a5e5a20..ba0042c6f9 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -34,8 +34,8 @@ Follow this link, [Create new OAuth App](https://github.com/settings/application 1. Set Application Name to `backstage-dev` or something along those lines. 1. You can set the Homepage URL to whatever you want to. 1. The Authorization Callback URL should match the redirect URI set in Backstage. - 1. Set this to `http://localhost:7007/api/auth/github` for local development. - 1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/api/auth/github` for non-local deployments. + 1. Set this to `http://localhost:7007/api/auth/github/handler/frame` for local development. + 1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/api/auth/github/handler/frame` for non-local deployments. ```bash export AUTH_GITHUB_CLIENT_ID=x diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index cc6962cdbe..cec3422d9c 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -669,6 +669,8 @@ export const readState: (stateString: string) => OAuthState; // @public (undocumented) export interface RouterOptions { + // (undocumented) + catalogApi?: CatalogApi; // (undocumented) config: Config; // (undocumented) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 4b3d7910e5..80f43e0b81 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.17.1-next.0", + "version": "0.17.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -60,7 +60,7 @@ "morgan": "^1.10.0", "node-cache": "^5.1.2", "node-fetch": "^2.6.7", - "openid-client": "^5.1.3", + "openid-client": "^5.2.1", "passport": "^0.6.0", "passport-auth0": "^1.4.3", "passport-bitbucket-oauth2": "^0.1.2", @@ -89,7 +89,7 @@ "@types/passport-saml": "^1.1.3", "@types/passport-strategy": "^0.2.35", "@types/xml2js": "^0.4.7", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index e4d9304892..96cd8d32cb 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -24,7 +24,7 @@ import { import { CatalogIdentityClient } from './CatalogIdentityClient'; describe('CatalogIdentityClient', () => { - const catalogApi: jest.Mocked = { + const catalogApi = { getLocationById: jest.fn(), getEntityByRef: jest.fn(), getEntities: jest.fn(), @@ -48,7 +48,7 @@ describe('CatalogIdentityClient', () => { catalogApi.getEntities.mockResolvedValueOnce({ items: [{} as UserEntity] }); tokenManager.getToken.mockResolvedValue({ token: 'my-token' }); const client = new CatalogIdentityClient({ - catalogApi, + catalogApi: catalogApi as Partial as CatalogApi, tokenManager, }); @@ -106,7 +106,7 @@ describe('CatalogIdentityClient', () => { tokenManager.getToken.mockResolvedValue({ token: 'my-token' }); const client = new CatalogIdentityClient({ - catalogApi, + catalogApi: catalogApi as Partial as CatalogApi, tokenManager, }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts index c0e351c90b..0162366f61 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/helpers.test.ts @@ -95,19 +95,19 @@ describe('helpers', () => { parseRequestToken(7, undefined as any), ).rejects.toMatchObject({ name: 'AuthenticationError', - message: 'Missing Google IAP header: x-goog-iap-jwt-assertion', + message: 'Missing Google IAP header', }); await expect( parseRequestToken(undefined, undefined as any), ).rejects.toMatchObject({ name: 'AuthenticationError', - message: 'Missing Google IAP header: x-goog-iap-jwt-assertion', + message: 'Missing Google IAP header', }); await expect( parseRequestToken('', undefined as any), ).rejects.toMatchObject({ name: 'AuthenticationError', - message: 'Missing Google IAP header: x-goog-iap-jwt-assertion', + message: 'Missing Google IAP header', }); }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts b/plugins/auth-backend/src/providers/gcp-iap/helpers.ts index c7e81adc73..26d9a8c904 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/helpers.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/helpers.ts @@ -17,7 +17,7 @@ import { AuthenticationError } from '@backstage/errors'; import { OAuth2Client, TokenPayload } from 'google-auth-library'; import { AuthHandler } from '../types'; -import { GcpIapResult, IAP_JWT_HEADER } from './types'; +import { GcpIapResult } from './types'; export function createTokenValidator( audience: string, @@ -52,9 +52,7 @@ export async function parseRequestToken( tokenValidator: (token: string) => Promise, ): Promise { if (typeof jwtToken !== 'string' || !jwtToken) { - throw new AuthenticationError( - `Missing Google IAP header: ${IAP_JWT_HEADER}`, - ); + throw new AuthenticationError('Missing Google IAP header'); } let payload: TokenPayload; diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts index b1b2a3f66e..c01db3a853 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.test.ts @@ -18,6 +18,7 @@ import express from 'express'; import request from 'supertest'; import { AuthResolverContext } from '../types'; import { GcpIapProvider } from './provider'; +import { DEFAULT_IAP_JWT_HEADER } from './types'; beforeEach(() => { jest.clearAllMocks(); @@ -28,44 +29,47 @@ describe('GcpIapProvider', () => { const signInResolver = jest.fn(); const tokenValidator = jest.fn(); - it('runs the happy path', async () => { - const provider = new GcpIapProvider({ - authHandler, - signInResolver, - tokenValidator, - resolverContext: {} as AuthResolverContext, - }); + it.each([undefined, 'x-custom-header'])( + 'runs the happy path', + async jwtHeader => { + const provider = new GcpIapProvider({ + authHandler, + signInResolver, + tokenValidator, + resolverContext: {} as AuthResolverContext, + jwtHeader: jwtHeader, + }); - // { "sub": "user:default/me", "ent": ["group:default/home"] } - const backstageToken = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc'; - const iapToken = { sub: 's', email: 'e@mail.com' }; + // { "sub": "user:default/me", "ent": ["group:default/home"] } + const backstageToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvbWUiLCJlbnQiOlsiZ3JvdXA6ZGVmYXVsdC9ob21lIl19.CbmAKzFErGmtsnpRxyPc7dHv7WEjb5lY6206YCzR_Rc'; + const iapToken = { sub: 's', email: 'e@mail.com' }; - authHandler.mockResolvedValueOnce({ email: 'e@mail.com' }); - signInResolver.mockResolvedValueOnce({ token: backstageToken }); - tokenValidator.mockResolvedValueOnce(iapToken); + authHandler.mockResolvedValueOnce({ email: 'e@mail.com' }); + signInResolver.mockResolvedValueOnce({ token: backstageToken }); + tokenValidator.mockResolvedValueOnce(iapToken); - const app = express(); - app.use('/refresh', provider.refresh.bind(provider)); + const app = express(); + app.use('/refresh', provider.refresh.bind(provider)); - const response = await request(app) - .get('/refresh') - .set('x-goog-iap-jwt-assertion', 'token'); + const header = jwtHeader || DEFAULT_IAP_JWT_HEADER; + const response = await request(app).get('/refresh').set(header, 'token'); - expect(response.status).toBe(200); - expect(response.get('content-type')).toBe( - 'application/json; charset=utf-8', - ); - expect(response.body).toEqual({ - backstageIdentity: { - token: backstageToken, - identity: { - type: 'user', - userEntityRef: 'user:default/me', - ownershipEntityRefs: ['group:default/home'], + expect(response.status).toBe(200); + expect(response.get('content-type')).toBe( + 'application/json; charset=utf-8', + ); + expect(response.body).toEqual({ + backstageIdentity: { + token: backstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/me', + ownershipEntityRefs: ['group:default/home'], + }, }, - }, - providerInfo: { iapToken }, - }); - }); + providerInfo: { iapToken }, + }); + }, + ); }); diff --git a/plugins/auth-backend/src/providers/gcp-iap/provider.ts b/plugins/auth-backend/src/providers/gcp-iap/provider.ts index 500b062d5b..7e77853737 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/provider.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/provider.ts @@ -29,24 +29,27 @@ import { defaultAuthHandler, parseRequestToken, } from './helpers'; -import { GcpIapResponse, GcpIapResult, IAP_JWT_HEADER } from './types'; +import { GcpIapResponse, GcpIapResult, DEFAULT_IAP_JWT_HEADER } from './types'; export class GcpIapProvider implements AuthProviderRouteHandlers { private readonly authHandler: AuthHandler; private readonly signInResolver: SignInResolver; private readonly tokenValidator: (token: string) => Promise; private readonly resolverContext: AuthResolverContext; + private readonly jwtHeader: string; constructor(options: { authHandler: AuthHandler; signInResolver: SignInResolver; tokenValidator: (token: string) => Promise; resolverContext: AuthResolverContext; + jwtHeader?: string; }) { this.authHandler = options.authHandler; this.signInResolver = options.signInResolver; this.tokenValidator = options.tokenValidator; this.resolverContext = options.resolverContext; + this.jwtHeader = options?.jwtHeader || DEFAULT_IAP_JWT_HEADER; } async start() {} @@ -55,7 +58,7 @@ export class GcpIapProvider implements AuthProviderRouteHandlers { async refresh(req: express.Request, res: express.Response): Promise { const result = await parseRequestToken( - req.header(IAP_JWT_HEADER), + req.header(this.jwtHeader), this.tokenValidator, ); @@ -103,6 +106,7 @@ export const gcpIap = createAuthProviderIntegration({ }) { return ({ config, resolverContext }) => { const audience = config.getString('audience'); + const jwtHeader = config.getOptionalString('jwtHeader'); const authHandler = options.authHandler ?? defaultAuthHandler; const signInResolver = options.signIn.resolver; @@ -113,6 +117,7 @@ export const gcpIap = createAuthProviderIntegration({ signInResolver, tokenValidator, resolverContext, + jwtHeader, }); }; }, diff --git a/plugins/auth-backend/src/providers/gcp-iap/types.ts b/plugins/auth-backend/src/providers/gcp-iap/types.ts index 25828b7d51..6519de64ac 100644 --- a/plugins/auth-backend/src/providers/gcp-iap/types.ts +++ b/plugins/auth-backend/src/providers/gcp-iap/types.ts @@ -20,7 +20,7 @@ import { AuthResponse } from '../types'; /** * The header name used by the IAP. */ -export const IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; +export const DEFAULT_IAP_JWT_HEADER = 'x-goog-iap-jwt-assertion'; /** * The data extracted from an IAP token. diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 8fd758f1b4..8013777415 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -28,7 +28,7 @@ import { TokenManager, } from '@backstage/backend-common'; import { assertError, NotFoundError } from '@backstage/errors'; -import { CatalogClient } from '@backstage/catalog-client'; +import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; @@ -48,6 +48,7 @@ export interface RouterOptions { tokenManager: TokenManager; tokenFactoryAlgorithm?: string; providerFactories?: ProviderFactories; + catalogApi?: CatalogApi; } /** @public */ @@ -62,6 +63,7 @@ export async function createRouter( tokenManager, tokenFactoryAlgorithm, providerFactories, + catalogApi, } = options; const router = Router(); @@ -78,7 +80,6 @@ export async function createRouter( logger: logger.child({ component: 'token-factory' }), algorithm: tokenFactoryAlgorithm, }); - const catalogApi = new CatalogClient({ discoveryApi: discovery }); const secret = config.getOptionalString('auth.session.secret'); if (secret) { @@ -114,7 +115,7 @@ export async function createRouter( allProviderFactories, )) { if (configuredProviders.includes(providerId)) { - logger.info(`Configuring provider, ${providerId}`); + logger.info(`Configuring auth provider: ${providerId}`); try { const provider = providerFactory({ providerId, @@ -127,7 +128,8 @@ export async function createRouter( logger, resolverContext: CatalogAuthResolverContext.create({ logger, - catalogApi, + catalogApi: + catalogApi ?? new CatalogClient({ discoveryApi: discovery }), tokenIssuer, tokenManager, }), diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index aac75b87e9..a1fb63b7ee 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-auth-node +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.2.8-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.2.7 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + ## 0.2.7-next.0 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 79dcc6d6b8..74392db58c 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.2.7-next.0", + "version": "0.2.8-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "lodash": "^4.17.21", - "msw": "^0.47.0", + "msw": "^0.49.0", "uuid": "^8.0.0" }, "files": [ diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index dfaffa8e35..290dc14ffd 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-azure-devops-backend +## 0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.18-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.17 + +### Patch Changes + +- 62f284e394: - Adjusted the asset parser to accept case sensitive + - Fixed fetching data that was using the deprecated function +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.3.17-next.1 + +### Patch Changes + +- 62f284e394: - Adjusted the asset parser to accept case sensitive + - Fixed fetching data that was using the deprecated function + ## 0.3.17-next.0 ### Patch Changes diff --git a/plugins/azure-devops-backend/README.md b/plugins/azure-devops-backend/README.md index 5dc0570bd2..301ae5b2b9 100644 --- a/plugins/azure-devops-backend/README.md +++ b/plugins/azure-devops-backend/README.md @@ -49,6 +49,7 @@ Here's how to get the backend up and running: return createRouter({ logger: env.logger, config: env.config, + reader: env.reader, }); } ``` diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 940b24ed44..e63715e55c 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.3.17-next.0", + "version": "0.3.18-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,7 +37,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts index 2bf2988a98..24c13020b6 100644 --- a/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops-backend/src/api/AzureDevOpsApi.ts @@ -408,14 +408,15 @@ export class AzureDevOpsApi { content: string; }> { const url = buildEncodedUrl(host, org, project, repo, 'README.md'); - const response = await this.urlReader.read(url); + const response = await this.urlReader.readUrl(url); + const buffer = await response.buffer(); const content = await replaceReadme( this.urlReader, host, org, project, repo, - response.toString(), + buffer.toString(), ); return { url, content }; } diff --git a/plugins/azure-devops-backend/src/utils/azure-devops-utils.test.ts b/plugins/azure-devops-backend/src/utils/azure-devops-utils.test.ts index f044a20968..74932c9653 100644 --- a/plugins/azure-devops-backend/src/utils/azure-devops-utils.test.ts +++ b/plugins/azure-devops-backend/src/utils/azure-devops-utils.test.ts @@ -167,30 +167,30 @@ describe('extractAssets', () => { it('should return assets', () => { const readme = ` ## Images - ![Image 1](./images/sample-4(2).png) + ![Image 1](./images/sample-4(2).PNG) ![Image 2](./images/cdCSj+-012340.jpg) ![Image 3](/images/test-4(2)))).jpeg) ![Image 4](./images/test-2211jd.webp) - ![Image 5](/images/sa)mple.gif) + ![Image 5](/images/sa)mple.GIf) `; const result = extractAssets(readme); expect(result).toEqual([ - '[Image 1](./images/sample-4(2).png)', + '[Image 1](./images/sample-4(2).PNG)', '[Image 2](./images/cdCSj+-012340.jpg)', '[Image 3](/images/test-4(2)))).jpeg)', '[Image 4](./images/test-2211jd.webp)', - '[Image 5](/images/sa)mple.gif)', + '[Image 5](/images/sa)mple.GIf)', ]); }); }); describe('extractPartsFromAsset', () => { it('should return parts from asset - PNG', () => { - const result = extractPartsFromAsset('[Image 1](./images/sample-4(2).png)'); + const result = extractPartsFromAsset('[Image 1](./images/sample-4(2).PNG)'); expect(result).toEqual({ label: 'Image 1', path: '/images/sample-4(2)', - ext: '.png', + ext: '.PNG', }); }); @@ -207,12 +207,12 @@ describe('extractPartsFromAsset', () => { it('should return parts from asset - JPEG', () => { const result = extractPartsFromAsset( - '[Image 2](/images/test-4(2)))).jpeg)', + '[Image 2](/images/test-4(2)))).JpEg)', ); expect(result).toEqual({ label: 'Image 2', path: '/images/test-4(2))))', - ext: '.jpeg', + ext: '.JpEg', }); }); @@ -247,10 +247,15 @@ describe('replaceReadme', () => { `; const reader: UrlReader = { - read: url => new Promise(resolve => resolve(Buffer.from(url))), + readUrl: url => + Promise.resolve({ + buffer: async () => Buffer.from(url), + etag: 'buffer', + stream: jest.fn(), + }), readTree: jest.fn(), search: jest.fn(), - readUrl: jest.fn(), + read: jest.fn(), }; const result = await replaceReadme( diff --git a/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts b/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts index 191486a925..0aafb54a0a 100644 --- a/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts +++ b/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts @@ -223,8 +223,9 @@ export async function replaceReadme( const { label, path, ext } = extractPartsFromAsset(filePath); const data = mime.lookup(ext); const url = buildEncodedUrl(host, org, project, repo, path + ext); - const buffer = await urlReader.read(url); - const file = await buffer.toString('base64'); + const response = await urlReader.readUrl(url); + const buffer = await response.buffer(); + const file = buffer.toString('base64'); return content.replace( filePath, `[${label}](data:${data};base64,${file})`, @@ -320,7 +321,7 @@ export function extractPartsFromAsset(content: string): { ext: string; } { const regExp = - /\[(.*?)\]\((?!https?:\/\/)(.*?)(\.png|\.jpg|\.jpeg|\.gif|\.webp)(.*)\)/; + /\[(.*?)\]\((?!https?:\/\/)(.*?)(\.png|\.jpg|\.jpeg|\.gif|\.webp)(.*)\)/i; const [_, label, path, ext] = regExp.exec(content) || []; return { ext, diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 1317547b7f..9ac60deffe 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/plugin-azure-devops +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.2.3-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + +## 0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.2.2-next.0 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index a2ffd77c84..a6ab993594 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.2.2-next.0", + "version": "0.2.3-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -56,7 +56,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/azure-sites-backend/CHANGELOG.md b/plugins/azure-sites-backend/CHANGELOG.md index b06742648f..515ef7a186 100644 --- a/plugins/azure-sites-backend/CHANGELOG.md +++ b/plugins/azure-sites-backend/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-azure-sites-backend +## 0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.0 + +### Minor Changes + +- 4a75ce761c: Azure Sites (Apps & Functions) support for a given entity. View the current status of the site, quickly jump to site's Overview page, or Log Stream page. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/config@1.0.4 + +## 0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-azure-sites-common@0.1.0-next.0 + ## 0.1.0-next.0 ### Minor Changes diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json index f6529b7a3d..dd233b0b58 100644 --- a/plugins/azure-sites-backend/package.json +++ b/plugins/azure-sites-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites-backend", - "version": "0.1.0-next.0", + "version": "0.1.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -48,7 +48,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/azure-sites-common/CHANGELOG.md b/plugins/azure-sites-common/CHANGELOG.md index 387e8d9ddd..f210cdf7bb 100644 --- a/plugins/azure-sites-common/CHANGELOG.md +++ b/plugins/azure-sites-common/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/plugin-azure-sites-common +## 0.1.0 + +### Minor Changes + +- 4a75ce761c: Azure Sites (Apps & Functions) support for a given entity. View the current status of the site, quickly jump to site's Overview page, or Log Stream page. + ## 0.1.0-next.0 ### Minor Changes diff --git a/plugins/azure-sites-common/package.json b/plugins/azure-sites-common/package.json index 568edaeff9..c1bf28c65b 100644 --- a/plugins/azure-sites-common/package.json +++ b/plugins/azure-sites-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-azure-sites-common", "description": "Common functionalities for the azure plugin", - "version": "0.1.0-next.0", + "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-sites/CHANGELOG.md b/plugins/azure-sites/CHANGELOG.md index 4e1b2bb12b..3cd14863b3 100644 --- a/plugins/azure-sites/CHANGELOG.md +++ b/plugins/azure-sites/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-azure-sites +## 0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0 + +## 0.1.0 + +### Minor Changes + +- 4a75ce761c: Azure Sites (Apps & Functions) support for a given entity. View the current status of the site, quickly jump to site's Overview page, or Log Stream page. + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/theme@0.2.16 + +## 0.1.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.0-next.0 ### Minor Changes diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json index feff8e8645..67e41e3424 100644 --- a/plugins/azure-sites/package.json +++ b/plugins/azure-sites/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites", - "version": "0.1.0-next.0", + "version": "0.1.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -57,7 +57,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index c88b7cf763..e0ccf6481e 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-badges-backend +## 0.1.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + ## 0.1.32-next.0 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index a8e465597c..eb2a40ea61 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.1.32-next.0", + "version": "0.1.33-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index 9dc60ea1b5..109b64231d 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -29,7 +29,19 @@ import { BadgeBuilder } from '../lib'; describe('createRouter', () => { let app: express.Express; let badgeBuilder: jest.Mocked; - let catalog: jest.Mocked; + const catalog = { + addLocation: jest.fn(), + getEntities: jest.fn(), + getEntityByRef: jest.fn(), + getLocationByRef: jest.fn(), + getLocationById: jest.fn(), + removeLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), + validateEntity: jest.fn(), + }; let config: Config; let discovery: PluginEndpointDiscovery; @@ -57,20 +69,6 @@ describe('createRouter', () => { createBadgeJson: jest.fn(), createBadgeSvg: jest.fn(), }; - catalog = { - addLocation: jest.fn(), - getEntities: jest.fn(), - getEntityByRef: jest.fn(), - getLocationByRef: jest.fn(), - getLocationById: jest.fn(), - removeLocationById: jest.fn(), - removeEntityByUid: jest.fn(), - refreshEntity: jest.fn(), - getEntityAncestors: jest.fn(), - getEntityFacets: jest.fn(), - validateEntity: jest.fn(), - }; - config = new ConfigReader({ backend: { baseUrl: 'http://127.0.0.1', @@ -81,7 +79,7 @@ describe('createRouter', () => { const router = await createRouter({ badgeBuilder, - catalog, + catalog: catalog as Partial as CatalogApi, config, discovery, }); @@ -95,7 +93,7 @@ describe('createRouter', () => { it('works', async () => { const router = await createRouter({ badgeBuilder, - catalog, + catalog: catalog as Partial as CatalogApi, config, discovery, }); diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index f736588933..0931d7b0df 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-badges +## 0.2.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.2.36-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.2.35 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.2.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.2.35-next.0 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 68d47e08ac..7c5faf9eb1 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.35-next.0", + "version": "0.2.36-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -54,7 +54,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index d0524bd9f5..905b53735c 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-bazaar-backend +## 0.2.2-next.1 + +### Patch Changes + +- 9b1891061c: Column `title` has replaced column `name` for `BazaarProject` in database +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-test-utils@0.1.31-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-test-utils@0.1.31-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/backend-test-utils@0.1.30 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-test-utils@0.1.30-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + ## 0.2.1-next.0 ### Patch Changes diff --git a/plugins/bazaar-backend/knexfile.js b/plugins/bazaar-backend/knexfile.js new file mode 100644 index 0000000000..c05af246c5 --- /dev/null +++ b/plugins/bazaar-backend/knexfile.js @@ -0,0 +1,28 @@ +/* + * 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. + */ + +// Update with your config settings. + +// This file makes it possible to run "yarn knex migrate:make some_file_name" +// to assist in making new migrations +module.exports = { + client: 'better-sqlite3', + connection: ':memory:', + useNullAsDefault: true, + migrations: { + directory: './migrations', + }, +}; diff --git a/plugins/bazaar-backend/migrations/20211014144054_init.js b/plugins/bazaar-backend/migrations/20211014144054_init.js index 5bf66698ac..503812225d 100644 --- a/plugins/bazaar-backend/migrations/20211014144054_init.js +++ b/plugins/bazaar-backend/migrations/20211014144054_init.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.createTable('metadata', table => { table.comment('The table of Bazaar metadata'); @@ -59,6 +64,9 @@ exports.up = async function up(knex) { }); }; +/** + * @param {import('knex').Knex} knex + */ exports.down = async function down(knex) { await knex.schema.dropTable('metadata'); await knex.schema.dropTable('members'); diff --git a/plugins/bazaar-backend/migrations/20211020073922_add_columns.js b/plugins/bazaar-backend/migrations/20211020073922_add_columns.js index ea3a89ea72..740eacb407 100644 --- a/plugins/bazaar-backend/migrations/20211020073922_add_columns.js +++ b/plugins/bazaar-backend/migrations/20211020073922_add_columns.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.alterTable('metadata', table => { table @@ -35,6 +40,9 @@ exports.up = async function up(knex) { }); }; +/** + * @param {import('knex').Knex} knex + */ exports.down = async function down(knex) { await knex.schema.alterTable('metadata', table => { table diff --git a/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js index 9dd8097ae2..0953d5c2b1 100644 --- a/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js +++ b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { if (knex.client.config.client.includes('sqlite3')) { await knex.schema.dropTable('metadata'); @@ -92,6 +97,9 @@ exports.up = async function up(knex) { } }; +/** + * @param {import('knex').Knex} knex + */ exports.down = async function down(knex) { if (knex.client.config.client.includes('sqlite3')) { await knex.schema.dropTable('metadata'); diff --git a/plugins/bazaar-backend/migrations/20220817150443_add_member_entity_ref.js b/plugins/bazaar-backend/migrations/20220817150443_add_member_entity_ref.js index 79b80ef20c..c7ac0ccf2c 100644 --- a/plugins/bazaar-backend/migrations/20220817150443_add_member_entity_ref.js +++ b/plugins/bazaar-backend/migrations/20220817150443_add_member_entity_ref.js @@ -14,12 +14,20 @@ * limitations under the License. */ +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ exports.up = async function up(knex) { await knex.schema.alterTable('members', table => { table.string('user_ref').nullable(); }); }; +/** + * @param {import('knex').Knex} knex + */ exports.down = async function down(knex) { return knex.schema.table('members', table => { table.dropColumn('user_ref'); diff --git a/plugins/bazaar-backend/migrations/20221121120212_rename_col_to_title.js b/plugins/bazaar-backend/migrations/20221121120212_rename_col_to_title.js new file mode 100644 index 0000000000..93de567f93 --- /dev/null +++ b/plugins/bazaar-backend/migrations/20221121120212_rename_col_to_title.js @@ -0,0 +1,35 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('metadata', table => { + table.renameColumn('name', 'title'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('metadata', table => { + table.renameColumn('title', 'name'); + }); +}; diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 2f0e766917..29c2189c13 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.2.1-next.0", + "version": "0.2.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts b/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts index 371d20ff13..be4f82f580 100644 --- a/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts +++ b/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts @@ -19,7 +19,7 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Knex as KnexType } from 'knex'; const bazaarProject: any = { - name: 'n1', + title: 'n1', entityRef: 'ref1', community: '', status: 'proposed', @@ -64,7 +64,7 @@ describe('DatabaseHandler', () => { await knex('metadata').insert({ entity_ref: bazaarProject.entityRef, - name: bazaarProject.name, + title: bazaarProject.title, description: bazaarProject.description, community: bazaarProject.community, status: bazaarProject.status, diff --git a/plugins/bazaar-backend/src/service/DatabaseHandler.ts b/plugins/bazaar-backend/src/service/DatabaseHandler.ts index 8fbe985a10..f75ea8b782 100644 --- a/plugins/bazaar-backend/src/service/DatabaseHandler.ts +++ b/plugins/bazaar-backend/src/service/DatabaseHandler.ts @@ -52,7 +52,7 @@ export class DatabaseHandler { private columns = [ 'metadata.id', 'metadata.entity_ref', - 'metadata.name', + 'metadata.title', 'metadata.description', 'metadata.status', 'metadata.updated_at', @@ -116,7 +116,7 @@ export class DatabaseHandler { async insertMetadata(bazaarProject: any) { const { - name, + title, entityRef, community, description, @@ -129,7 +129,7 @@ export class DatabaseHandler { await this.client .insert({ - name, + title, entity_ref: entityRef, community, description, @@ -145,7 +145,7 @@ export class DatabaseHandler { async updateMetadata(bazaarProject: any) { const { - name, + title, id, entityRef, community, @@ -158,7 +158,7 @@ export class DatabaseHandler { } = bazaarProject; return await this.client('metadata').where({ id: id }).update({ - name, + title, entity_ref: entityRef, description, community, diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index 7ff38b63f8..f5c28e2844 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-bazaar +## 0.2.1-next.1 + +### Patch Changes + +- 9b1891061c: `HomePageBazaarInfoCard` is now displaying `title` instead of `name`. Title is a string that doesn't have to be URL friendly. + The BazaarOverviewCard have the new property `fullHeight`. Link in `BazaarOverviewCard`is moved to header in card. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-catalog@1.6.2-next.1 + - @backstage/cli@0.21.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/cli@0.21.2-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/plugin-catalog@1.6.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.2.0 + +### Minor Changes + +- 28b39e0e0e: The limit prop of BazaarOverviewCard has been removed entirely, and instead replaced with a new optional boolean prop `fullWidth`. The BazaarOverviewCard now always use full height without fixed width. Also fixed problem with link to Bazaar. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/cli@0.21.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-catalog@1.6.1 + - @backstage/catalog-client@1.1.2 + - @backstage/errors@1.1.3 + +## 0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.21.0-next.1 + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-catalog@1.6.1-next.1 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.2.0-next.0 ### Minor Changes diff --git a/plugins/bazaar/README.md b/plugins/bazaar/README.md index 40b2a8afbe..2ad141935e 100644 --- a/plugins/bazaar/README.md +++ b/plugins/bazaar/README.md @@ -80,13 +80,15 @@ export const homePage = ( + + -+ ++ + {/* ...other homepage items */} ``` -The property `fullWidth` is optional and can be used to adjust the card to fit a grid with column width 12. +The property `title` is optional and can be used to customize the title in the card header. If no title is submitted the default titles `Bazaar Random Projects` or `Bazaar Latest Projects` are displayed. + +The properties `fullHeight` and `fullWidth` are also optional and can be used to adjust the cards styling. ## How does the Bazaar work? @@ -106,7 +108,7 @@ To add a project to the bazaar, simply click on the `add-project` button and fil The following fields are mandatory: -- name - name of the project on URL safe format +- title - title of the project - description - present your idea and what skills you are looking for - status - whether or not the project has started - size - small, medium or large diff --git a/plugins/bazaar/api-report.md b/plugins/bazaar/api-report.md index c12c95a1f7..d7c463888a 100644 --- a/plugins/bazaar/api-report.md +++ b/plugins/bazaar/api-report.md @@ -15,8 +15,10 @@ export const BazaarOverviewCard: ( // @public (undocumented) export type BazaarOverviewCardProps = { + title?: string; order: 'latest' | 'random'; fullWidth?: boolean; + fullHeight?: boolean; }; // @public (undocumented) diff --git a/plugins/bazaar/media/layout.png b/plugins/bazaar/media/layout.png index cc02a374a3..86c95f1e6e 100644 Binary files a/plugins/bazaar/media/layout.png and b/plugins/bazaar/media/layout.png differ diff --git a/plugins/bazaar/media/overviewCard.png b/plugins/bazaar/media/overviewCard.png index 676764f6e4..ef41ca48e5 100644 Binary files a/plugins/bazaar/media/overviewCard.png and b/plugins/bazaar/media/overviewCard.png differ diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index eb0542a504..607d91a731 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.2.0-next.0", + "version": "0.2.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/bazaar/plugin-bazaar.api.md b/plugins/bazaar/plugin-bazaar.api.md deleted file mode 100644 index 33a674ffc4..0000000000 --- a/plugins/bazaar/plugin-bazaar.api.md +++ /dev/null @@ -1,25 +0,0 @@ -## API Report File for "@backstage/plugin-bazaar" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts -/// - -import * as _backstage_core_plugin_api from '@backstage/core-plugin-api'; - -// @public (undocumented) -export const BazaarPage: () => JSX.Element; - -// @public (undocumented) -export const bazaarPlugin: _backstage_core_plugin_api.BackstagePlugin< - { - root: _backstage_core_plugin_api.RouteRef; - }, - {} ->; - -// @public (undocumented) -export const EntityBazaarInfoCard: () => JSX.Element; - -// (No @packageDocumentation comment for this package) -``` diff --git a/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx b/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx index 749fcf3cff..45392206d9 100644 --- a/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx +++ b/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx @@ -42,8 +42,7 @@ export const AddProjectDialog = ({ const [selectedEntity, setSelectedEntity] = useState(null); const defaultValues = { - name: '', - title: 'Add project', + title: '', community: '', description: '', status: 'proposed' as Status, @@ -65,6 +64,7 @@ export const AddProjectDialog = ({ reset: UseFormReset, ) => { const formValues = getValues(); + const response = await bazaarApi.addProject({ ...formValues, entityRef: selectedEntity ? stringifyEntityRef(selectedEntity) : null, diff --git a/plugins/bazaar/src/components/BazaarOverviewCard/BazaarOverviewCard.tsx b/plugins/bazaar/src/components/BazaarOverviewCard/BazaarOverviewCard.tsx index d64e36baeb..c58ee3db1c 100644 --- a/plugins/bazaar/src/components/BazaarOverviewCard/BazaarOverviewCard.tsx +++ b/plugins/bazaar/src/components/BazaarOverviewCard/BazaarOverviewCard.tsx @@ -24,13 +24,17 @@ import type { BazaarProject } from '../../types'; import { bazaarApiRef } from '../../api'; import { fetchCatalogItems } from '../../util/fetchMethods'; import { parseBazaarProject } from '../../util/parseMethods'; -import { ErrorPanel, InfoCard } from '@backstage/core-components'; +import { ErrorPanel, InfoCard, Link } from '@backstage/core-components'; import { bazaarPlugin } from '../../plugin'; +import { IconButton } from '@material-ui/core'; +import StorefrontIcon from '@material-ui/icons/Storefront'; /** @public */ export type BazaarOverviewCardProps = { + title?: string; order: 'latest' | 'random'; fullWidth?: boolean; + fullHeight?: boolean; }; const getUnlinkedCatalogEntities = ( @@ -48,11 +52,14 @@ const getUnlinkedCatalogEntities = ( /** @public */ export const BazaarOverviewCard = (props: BazaarOverviewCardProps) => { - const { order, fullWidth = false } = props; + const { title, order, fullWidth = false, fullHeight = false } = props; const bazaarApi = useApi(bazaarApiRef); const catalogApi = useApi(catalogApiRef); const root = useRouteRef(bazaarPlugin.routes.root); + const defaultTitle = + order === 'latest' ? 'Bazaar Latest Projects' : 'Bazaar Random Projects'; + const bazaarLink = { title: 'Go to Bazaar', link: `${root()}`, @@ -124,10 +131,14 @@ export const BazaarOverviewCard = (props: BazaarOverviewCardProps) => { return ( + + + + } - deepLink={bazaarLink} > { catalogEntities={unlinkedCatalogEntities || []} useTablePagination={false} gridSize={fullWidth ? 2 : 4} + height={fullHeight ? '13rem' : '7rem'} /> ); diff --git a/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx b/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx index 641ae97f95..82ab9df89d 100644 --- a/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx +++ b/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx @@ -17,7 +17,6 @@ import React from 'react'; import { Grid, - makeStyles, Card, CardContent, Typography, @@ -31,13 +30,6 @@ import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { StatusTag } from '../StatusTag'; import { Member, BazaarProject } from '../../types'; -const useStyles = makeStyles({ - break: { - wordBreak: 'break-word', - textAlign: 'justify', - }, -}); - type Props = { bazaarProject: BazaarProject; members: Member[]; @@ -51,7 +43,6 @@ export const CardContentFields = ({ descriptionSize, membersSize, }: Props) => { - const classes = useStyles(); const catalogEntityRoute = useRouteRef(entityRouteRef); return ( @@ -64,12 +55,7 @@ export const CardContentFields = ({ {bazaarProject.description .split('\n') .map((str: string, i: number) => ( - + {str} ))} @@ -114,7 +100,6 @@ export const CardContentFields = ({ picture={member.picture} /> - {bazaarProject.name} + + {bazaarProject.title} , ' from the Bazaar?', ]} diff --git a/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx b/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx index 0c9da1ed24..a2e5fe151b 100644 --- a/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx +++ b/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx @@ -173,7 +173,7 @@ export const EntityBazaarInfoContent = ({ {parseEntityRef(bazaarProject.entityRef!).name} , ' from ', - {bazaarProject.name}, + {bazaarProject.title}, ' ?', ]} type="unlink" @@ -182,7 +182,7 @@ export const EntityBazaarInfoContent = ({ )} {bazaarProject?.name!}

} + title={

{bazaarProject?.title!}

} action={
, ' from ', - {bazaarProject.value?.name}, + {bazaarProject.value?.title}, ' ?', ]} type="unlink" @@ -257,7 +257,7 @@ export const HomePageBazaarInfoCard = ({ - {bazaarProject.value?.name || initProject.name} + {bazaarProject.value?.title || initProject.title}

} action={ diff --git a/plugins/bazaar/src/components/InputField/InputField.tsx b/plugins/bazaar/src/components/InputField/InputField.tsx index cae22a1313..a2c8ca4ac4 100644 --- a/plugins/bazaar/src/components/InputField/InputField.tsx +++ b/plugins/bazaar/src/components/InputField/InputField.tsx @@ -30,7 +30,7 @@ type Rules = { }; type Props = { - inputType: 'description' | 'community' | 'responsible' | 'name'; + inputType: 'description' | 'community' | 'responsible' | 'title'; error?: FieldError | undefined; control: Control; helperText?: string; diff --git a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx index fcae411519..9193215804 100644 --- a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx +++ b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx @@ -34,7 +34,7 @@ type Props = { project: BazaarProject; fetchBazaarProjects: () => Promise; catalogEntities: Entity[]; - fullHeight?: boolean; + height?: string; }; const useStyles = makeStyles({ @@ -48,7 +48,6 @@ const useStyles = makeStyles({ WebkitLineClamp: 7, WebkitBoxOrient: 'vertical', overflow: 'hidden', - textAlign: 'justify', }, memberCount: { float: 'right', @@ -60,6 +59,7 @@ const useStyles = makeStyles({ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', + height: '5rem', }, }); @@ -67,10 +67,11 @@ export const ProjectCard = ({ project, fetchBazaarProjects, catalogEntities, + height, }: Props) => { const classes = useStyles(); const [openCard, setOpenCard] = useState(false); - const { id, name, status, updatedAt, description, membersCount } = project; + const { id, title, status, updatedAt, description, membersCount } = project; const handleClose = () => { setOpenCard(false); @@ -93,7 +94,7 @@ export const ProjectCard = ({ classes={{ root: classes.header }} title={ - {name} + {title} } subtitle={`updated ${DateTime.fromISO( @@ -102,7 +103,7 @@ export const ProjectCard = ({ base: DateTime.now(), })}`} /> - + {Number(membersCount) === Number(1) diff --git a/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx b/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx index 2f8b9e9cb9..ec7312576d 100644 --- a/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx +++ b/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx @@ -89,16 +89,14 @@ export const ProjectDialog = ({ - @@ -142,7 +140,7 @@ export const ProjectDialog = ({ pattern: RegExp('^(https?)://'), }} inputType="community" - helperText="please enter a link starting with http/https" + helperText="Please enter a link starting with http/https" placeholder="Community link to e.g. Teams or Discord" /> diff --git a/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx b/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx index 7dd82edf94..216cf50c2f 100644 --- a/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx +++ b/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx @@ -27,6 +27,7 @@ type Props = { catalogEntities: Entity[]; useTablePagination?: boolean; gridSize?: GridSize; + height: string; }; const useStyles = makeStyles({ @@ -55,6 +56,7 @@ export const ProjectPreview = ({ catalogEntities, useTablePagination = true, gridSize = 2, + height, }: Props) => { const classes = useStyles(); const [page, setPage] = useState(1); @@ -90,6 +92,7 @@ export const ProjectPreview = ({ key={i} fetchBazaarProjects={fetchBazaarProjects} catalogEntities={catalogEntities} + height={height} /> ); diff --git a/plugins/bazaar/src/components/SortView/SortView.tsx b/plugins/bazaar/src/components/SortView/SortView.tsx index 75f5bbeb34..68b63a6885 100644 --- a/plugins/bazaar/src/components/SortView/SortView.tsx +++ b/plugins/bazaar/src/components/SortView/SortView.tsx @@ -27,7 +27,7 @@ import { BazaarProject } from '../../types'; import { bazaarApiRef } from '../../api'; import { Alert } from '@material-ui/lab'; import SearchBar from 'material-ui-search-bar'; -import { sortByDate, sortByMembers, sortByName } from '../../util/sortMethods'; +import { sortByDate, sortByMembers, sortByTitle } from '../../util/sortMethods'; import { SortMethodSelector } from '../SortMethodSelector'; import { fetchCatalogItems } from '../../util/fetchMethods'; import { parseBazaarProject } from '../../util/parseMethods'; @@ -71,7 +71,7 @@ export const SortView = () => { const bazaarApi = useApi(bazaarApiRef); const catalogApi = useApi(catalogApiRef); const classes = useStyles(); - const sortMethods = [sortByDate, sortByName, sortByMembers]; + const sortMethods = [sortByDate, sortByTitle, sortByMembers]; const [sortMethodNbr, setSortMethodNbr] = useState(0); const [openAdd, setOpenAdd] = useState(false); const [searchValue, setSearchValue] = useState(''); @@ -99,7 +99,7 @@ export const SortView = () => { const getSearchResults = () => { return bazaarProjects.value - ?.filter(project => project.name.includes(searchValue)) + ?.filter(project => project.title.includes(searchValue)) .sort(sortMethods[sortMethodNbr]); }; @@ -199,6 +199,7 @@ export const SortView = () => { bazaarProjects={getSearchResults() || []} fetchBazaarProjects={fetchBazaarProjects} catalogEntities={unlinkedCatalogEntities || []} + height="13rem" /> diff --git a/plugins/bazaar/src/types.ts b/plugins/bazaar/src/types.ts index e698f21264..055ec2fcab 100644 --- a/plugins/bazaar/src/types.ts +++ b/plugins/bazaar/src/types.ts @@ -27,7 +27,7 @@ export type Status = 'ongoing' | 'proposed'; export type Size = 'small' | 'medium' | 'large'; export type BazaarProject = { - name: string; + title: string; id: number; entityRef?: string; community: string; @@ -42,7 +42,7 @@ export type BazaarProject = { }; export type FormValues = { - name: string; + title: string; description: string; community: string; status: string; diff --git a/plugins/bazaar/src/util/parseMethods.ts b/plugins/bazaar/src/util/parseMethods.ts index b32c5d42d3..b93f70c9f1 100644 --- a/plugins/bazaar/src/util/parseMethods.ts +++ b/plugins/bazaar/src/util/parseMethods.ts @@ -20,7 +20,7 @@ export const parseBazaarProject = (metadata: any): BazaarProject => { return { id: metadata.id, entityRef: metadata.entity_ref, - name: metadata.name, + title: metadata.title, community: metadata.community, description: metadata.description, status: metadata.status, diff --git a/plugins/bazaar/src/util/sortMethods.ts b/plugins/bazaar/src/util/sortMethods.ts index d75639edca..d7ef71c8de 100644 --- a/plugins/bazaar/src/util/sortMethods.ts +++ b/plugins/bazaar/src/util/sortMethods.ts @@ -26,10 +26,10 @@ export const sortByDate = (a: BazaarProject, b: BazaarProject): number => { return dateB - dateA; }; -export const sortByName = (a: BazaarProject, b: BazaarProject) => { - if (a.name < b.name) { +export const sortByTitle = (a: BazaarProject, b: BazaarProject) => { + if (a.title < b.title) { return -1; - } else if (a.name > b.name) { + } else if (a.title > b.title) { return 1; } return 0; diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index d3ab58bf5a..672e06f8df 100644 --- a/plugins/bitbucket-cloud-common/CHANGELOG.md +++ b/plugins/bitbucket-cloud-common/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-bitbucket-cloud-common +## 0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.4.1-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/integration@1.4.1-next.0 + +## 0.2.1 + +### Patch Changes + +- d089fbe7dc: Add interfaces for Bitbucket Cloud (webhook) events. +- Updated dependencies + - @backstage/integration@1.4.0 + ## 0.2.1-next.0 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/api-report.md b/plugins/bitbucket-cloud-common/api-report.md index d66b8b9a70..5182579a4b 100644 --- a/plugins/bitbucket-cloud-common/api-report.md +++ b/plugins/bitbucket-cloud-common/api-report.md @@ -24,6 +24,57 @@ export class BitbucketCloudClient { ): WithPagination; } +// @public (undocumented) +export namespace Events { + // (undocumented) + export interface Change { + // (undocumented) + closed: boolean; + // (undocumented) + commits: Models.Commit[]; + // (undocumented) + created: boolean; + // (undocumented) + forced: boolean; + // (undocumented) + links: ChangeLinks; + // (undocumented) + new: Models.Branch; + // (undocumented) + old: Models.Branch; + // (undocumented) + truncated: boolean; + } + // (undocumented) + export interface ChangeLinks { + // (undocumented) + commits: Models.Link; + // (undocumented) + diff: Models.Link; + // (undocumented) + html: Models.Link; + } + // (undocumented) + export interface RepoEvent { + // (undocumented) + actor: Models.Account; + // (undocumented) + repository: Models.Repository & { + workspace: Models.Workspace; + }; + } + // (undocumented) + export interface RepoPush { + // (undocumented) + changes: Change[]; + } + // (undocumented) + export interface RepoPushEvent extends RepoEvent { + // (undocumented) + push: RepoPush; + } +} + // @public (undocumented) export type FilterAndSortOptions = { q?: string; @@ -340,6 +391,37 @@ export namespace Models { // (undocumented) self?: Link; } + export interface Workspace extends ModelObject { + // (undocumented) + created_on?: string; + is_private?: boolean; + // (undocumented) + links?: WorkspaceLinks; + name?: string; + slug?: string; + // (undocumented) + updated_on?: string; + uuid?: string; + } + // (undocumented) + export interface WorkspaceLinks { + // (undocumented) + avatar?: Link; + // (undocumented) + html?: Link; + // (undocumented) + members?: Link; + // (undocumented) + owners?: Link; + // (undocumented) + projects?: Link; + // (undocumented) + repositories?: Link; + // (undocumented) + self?: Link; + // (undocumented) + snippets?: Link; + } } // @public (undocumented) diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index fbc63e4497..d43b716d7f 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitbucket-cloud-common", "description": "Common functionalities for bitbucket-cloud plugins", - "version": "0.2.1-next.0", + "version": "0.2.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,7 +33,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@openapitools/openapi-generator-cli": "^2.4.26", - "msw": "^0.47.0", + "msw": "^0.49.0", "ts-morph": "^15.0.0" }, "files": [ diff --git a/plugins/bitbucket-cloud-common/scripts/reduce-models.js b/plugins/bitbucket-cloud-common/scripts/reduce-models.js index 9d458f4e74..115020493c 100755 --- a/plugins/bitbucket-cloud-common/scripts/reduce-models.js +++ b/plugins/bitbucket-cloud-common/scripts/reduce-models.js @@ -30,6 +30,14 @@ const modelsModule = modelsFile.getModuleOrThrow('Models'); const clientFile = project.getSourceFile('src/BitbucketCloudClient.ts'); const clientClass = clientFile.getClassOrThrow('BitbucketCloudClient'); +const eventsFile = project.getSourceFile('src/events/index.ts'); +const eventsModule = eventsFile.getModuleOrThrow('Events'); +const eventsStmts = [ + ...eventsModule.getClasses(), + ...eventsModule.getInterfaces(), + ...eventsModule.getTypeAliases(), +]; + /** * Returns an array of the unique items of the provided array. * @@ -79,7 +87,11 @@ function referencedModelsIdentifiers(stmt, processed) { } // all directly or transitively referenced/used `Models.[...]` are allowed to stay -const allowed = referencedModelsIdentifiers(clientClass); +const processed = []; +const allowed = referencedModelsIdentifiers(clientClass, processed); +allowed.push( + ...eventsStmts.flatMap(stmt => referencedModelsIdentifiers(stmt, processed)), +); // remove everything not part of the "allow list" modelsModule diff --git a/plugins/bitbucket-cloud-common/src/events/index.ts b/plugins/bitbucket-cloud-common/src/events/index.ts new file mode 100644 index 0000000000..53658be032 --- /dev/null +++ b/plugins/bitbucket-cloud-common/src/events/index.ts @@ -0,0 +1,56 @@ +/* + * 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 { Models } from '../models'; + +// source: https://support.atlassian.com/bitbucket-cloud/docs/event-payloads + +/** @public */ +export namespace Events { + /** @public */ + export interface RepoEvent { + repository: Models.Repository & { workspace: Models.Workspace }; + actor: Models.Account; + } + + /** @public */ + export interface RepoPushEvent extends RepoEvent { + push: RepoPush; + } + + /** @public */ + export interface RepoPush { + changes: Change[]; + } + + /** @public */ + export interface Change { + old: Models.Branch; + new: Models.Branch; + truncated: boolean; + created: boolean; + forced: boolean; + closed: boolean; + links: ChangeLinks; + commits: Models.Commit[]; + } + + /** @public */ + export interface ChangeLinks { + commits: Models.Link; + diff: Models.Link; + html: Models.Link; + } +} diff --git a/plugins/bitbucket-cloud-common/src/index.ts b/plugins/bitbucket-cloud-common/src/index.ts index 4edf148240..4a9b00e89b 100644 --- a/plugins/bitbucket-cloud-common/src/index.ts +++ b/plugins/bitbucket-cloud-common/src/index.ts @@ -21,6 +21,7 @@ */ export * from './BitbucketCloudClient'; +export * from './events'; export * from './models'; export * from './pagination'; export * from './types'; diff --git a/plugins/bitbucket-cloud-common/src/models/index.ts b/plugins/bitbucket-cloud-common/src/models/index.ts index 580f23a13f..9d25bbb873 100644 --- a/plugins/bitbucket-cloud-common/src/models/index.ts +++ b/plugins/bitbucket-cloud-common/src/models/index.ts @@ -518,4 +518,46 @@ export namespace Models { repositories?: Link; self?: Link; } + + /** + * A Bitbucket workspace. + * Workspaces are used to organize repositories. + * @public + */ + export interface Workspace extends ModelObject { + created_on?: string; + /** + * Indicates whether the workspace is publicly accessible, or whether it is + * private to the members and consequently only visible to members. + */ + is_private?: boolean; + links?: WorkspaceLinks; + /** + * The name of the workspace. + */ + name?: string; + /** + * The short label that identifies this workspace. + */ + slug?: string; + updated_on?: string; + /** + * The workspace's immutable id. + */ + uuid?: string; + } + + /** + * @public + */ + export interface WorkspaceLinks { + avatar?: Link; + html?: Link; + members?: Link; + owners?: Link; + projects?: Link; + repositories?: Link; + self?: Link; + snippets?: Link; + } } diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index 2ec214ea00..985c3ef123 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-bitrise +## 0.1.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.39-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.38 + +### Patch Changes + +- 43afded227: Updated recharts to v2.0.0 and fixed typing issues +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.1.38-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.38-next.0 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index cb23af3b90..f3f40a3767 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.38-next.0", + "version": "0.1.39-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "@types/recharts": "^1.8.15", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.test.tsx b/plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.test.tsx index 2dcffc3bb3..7992e08f37 100644 --- a/plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.test.tsx +++ b/plugins/bitrise/src/components/BitriseBuildDetailsDialog/BitriseBuildDetailsDialog.test.tsx @@ -46,7 +46,7 @@ describe('BitriseArtifactsComponent', () => { const rendered = renderComponent(); const btn = await rendered.findByTestId('btn'); - expect(await rendered.queryByText('VISIBLE')).not.toBeInTheDocument(); + expect(rendered.queryByText('VISIBLE')).not.toBeInTheDocument(); btn.click(); diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index 59ab7970f2..0abb647ca2 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.1.11 + +### Patch Changes + +- bae3617be5: `AwsS3EntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + https://backstage.io/docs/integrations/aws-s3/discovery + +- defb389ecd: Add `awsS3EntityProviderCatalogModule` (new backend-plugin-api, alpha). +- a6d779d58a: Remove explicit default visibility at `config.d.ts` files. + + ```ts + /** + * @visibility backend + */ + ``` + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + ## 0.1.11-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index a000ee9d84..7948e3cbde 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", "description": "A Backstage catalog backend module that helps integrate towards AWS", - "version": "0.1.11-next.0", + "version": "0.1.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.test.ts index 3167291bca..c0d36ad3c2 100644 --- a/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.test.ts @@ -15,11 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { - configServiceRef, - loggerServiceRef, - schedulerServiceRef, -} from '@backstage/backend-plugin-api'; +import { coreServices } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskScheduleDefinition, @@ -66,9 +62,9 @@ describe('awsS3EntityProviderCatalogModule', () => { await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], services: [ - [configServiceRef, config], - [loggerServiceRef, getVoidLogger()], - [schedulerServiceRef, scheduler], + [coreServices.config, config], + [coreServices.logger, getVoidLogger()], + [coreServices.scheduler, scheduler], ], features: [awsS3EntityProviderCatalogModule()], }); diff --git a/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.ts b/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.ts index 79269ad53a..fa49d1e4c7 100644 --- a/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-aws/src/service/AwsS3EntityProviderCatalogModule.ts @@ -15,11 +15,9 @@ */ import { + coreServices, createBackendModule, loggerToWinstonLogger, - configServiceRef, - loggerServiceRef, - schedulerServiceRef, } from '@backstage/backend-plugin-api'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; import { AwsS3EntityProvider } from '../providers'; @@ -35,10 +33,10 @@ export const awsS3EntityProviderCatalogModule = createBackendModule({ register(env) { env.registerInit({ deps: { - config: configServiceRef, + config: coreServices.config, catalog: catalogProcessingExtensionPoint, - logger: loggerServiceRef, - scheduler: schedulerServiceRef, + logger: coreServices.logger, + scheduler: coreServices.scheduler, }, async init({ config, catalog, logger, scheduler }) { catalog.addEntityProvider( diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index 7fed636045..fd6d29410b 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.1.10-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.1.9 + +### Patch Changes + +- 87ff05892d: `AzureDevOpsEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + https://backstage.io/docs/integrations/azure/discovery + +- 0ca399b31b: Add `azureDevOpsEntityProviderCatalogModule` (new backend-plugin-api, alpha). +- 4c9f7847e4: Updated dependency `msw` to `^0.48.0` while moving it to be a dev dependency. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + ## 0.1.9-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index 204321f876..cabab2eb35 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", "description": "A Backstage catalog backend module that helps integrate towards Azure", - "version": "0.1.9-next.0", + "version": "0.1.10-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -44,7 +44,6 @@ "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", "lodash": "^4.17.21", - "msw": "^0.47.0", "node-fetch": "^2.6.7", "uuid": "^8.0.0", "winston": "^3.2.1" @@ -53,7 +52,8 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", - "luxon": "^3.0.0" + "luxon": "^3.0.0", + "msw": "^0.49.0" }, "files": [ "alpha", diff --git a/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.test.ts index e3a0449a8e..a11e6b14f2 100644 --- a/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.test.ts @@ -15,11 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { - configServiceRef, - loggerServiceRef, - schedulerServiceRef, -} from '@backstage/backend-plugin-api'; +import { coreServices } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskScheduleDefinition, @@ -69,9 +65,9 @@ describe('azureDevOpsEntityProviderCatalogModule', () => { await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], services: [ - [configServiceRef, config], - [loggerServiceRef, getVoidLogger()], - [schedulerServiceRef, scheduler], + [coreServices.config, config], + [coreServices.logger, getVoidLogger()], + [coreServices.scheduler, scheduler], ], features: [azureDevOpsEntityProviderCatalogModule()], }); diff --git a/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.ts index f3621bee23..0d0ebdb591 100644 --- a/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-azure/src/service/AzureDevOpsEntityProviderCatalogModule.ts @@ -17,9 +17,7 @@ import { createBackendModule, loggerToWinstonLogger, - configServiceRef, - loggerServiceRef, - schedulerServiceRef, + coreServices, } from '@backstage/backend-plugin-api'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; import { AzureDevOpsEntityProvider } from '../providers'; @@ -35,10 +33,10 @@ export const azureDevOpsEntityProviderCatalogModule = createBackendModule({ register(env) { env.registerInit({ deps: { - config: configServiceRef, + config: coreServices.config, catalog: catalogProcessingExtensionPoint, - logger: loggerServiceRef, - scheduler: schedulerServiceRef, + logger: coreServices.logger, + scheduler: coreServices.scheduler, }, async init({ config, catalog, logger, scheduler }) { catalog.addEntityProvider( diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index 47cc94f257..47fc6b38c7 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,112 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.1.6-next.1 + +### Patch Changes + +- 4eb0bce299: Fix repo:push topic not matching `BitbucketCloudEventRouter`. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.2-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## 0.1.6-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + +## 0.1.5 + +### Patch Changes + +- d089fbe7dc: Handle Bitbucket Cloud `repo:push` events at the `BitbucketCloudEntityProvider` + by subscribing to the topic `bitbucketCloud.repo:push.` + + Implements `EventSubscriber` to receive events for the topic `bitbucketCloud.repo:push`. + + On `repo:push`, the affected repository will be refreshed. + This includes adding new Location entities, refreshing existing ones, + and removing obsolete ones. + + To support this, a new annotation `bitbucket.org/repo-url` was added + to Location entities. + + A full refresh will require 1 API call to Bitbucket Cloud to discover all catalog files. + When we handle one `repo:push` event, we also need 1 API call in order to know + which catalog files exist. + This may lead to more discovery-related API calls (code search). + The main cause for hitting the rate limits are Locations refresh-related operations. + + A reduction of total API calls to reduce the rate limit issues can only be achieved in + combination with + + 1. reducing the full refresh frequency (e.g., to monthly) + 2. reducing the frequency of general Location refresh operations by the processing loop + + For (2.), it is not possible to reduce the frequency only for Bitbucket Cloud-related + Locations though. + + Further optimizations might be required to resolve the rate limit issue. + + **Installation and Migration** + + Please find more information at + https://backstage.io/docs/integrations/bitbucketCloud/discovery, + in particular the section about "_Installation with Events Support_". + + In case of the new backend-plugin-api _(alpha)_ the module will take care of + registering itself at both. + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-events-node@0.1.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/plugin-catalog-common@1.0.8 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.1-next.0 + ## 0.1.5-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md index 162ff8f67e..480395e477 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/api-report.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/api-report.md @@ -4,24 +4,33 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; +import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { EntityProvider } from '@backstage/plugin-catalog-backend'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import { EventParams } from '@backstage/plugin-events-node'; +import { Events } from '@backstage/plugin-bitbucket-cloud-common'; +import { EventSubscriber } from '@backstage/plugin-events-node'; import { Logger } from 'winston'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; +import { TokenManager } from '@backstage/backend-common'; // @public -export class BitbucketCloudEntityProvider implements EntityProvider { +export class BitbucketCloudEntityProvider + implements EntityProvider, EventSubscriber +{ // (undocumented) connect(connection: EntityProviderConnection): Promise; // (undocumented) static fromConfig( config: Config, options: { + catalogApi?: CatalogApi; logger: Logger; schedule?: TaskRunner; scheduler?: PluginTaskScheduler; + tokenManager?: TokenManager; }, ): BitbucketCloudEntityProvider[]; // (undocumented) @@ -29,7 +38,13 @@ export class BitbucketCloudEntityProvider implements EntityProvider { // (undocumented) getTaskId(): string; // (undocumented) + onEvent(params: EventParams): Promise; + // (undocumented) + onRepoPush(event: Events.RepoPushEvent): Promise; + // (undocumented) refresh(logger: Logger): Promise; + // (undocumented) + supportsEventTopics(): string[]; } // @alpha (undocumented) diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 93a9407aa1..2aa09b4a64 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", - "version": "0.1.5-next.0", + "version": "0.1.6-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,13 +33,18 @@ "clean": "backstage-cli package clean" }, "dependencies": { + "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-bitbucket-cloud-common": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", "uuid": "^8.0.0", "winston": "^3.2.1" }, @@ -48,7 +53,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "luxon": "^3.0.0", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "alpha", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts index 7d780cd064..367e95b99b 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.test.ts @@ -14,18 +14,27 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; +import { getVoidLogger, TokenManager } from '@backstage/backend-common'; import { PluginTaskScheduler, TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { ConfigReader } from '@backstage/config'; -import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import { BitbucketCloudEntityProvider } from './BitbucketCloudEntityProvider'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity, LocationEntity } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { + EntityProviderConnection, + locationSpecToLocationEntity, +} from '@backstage/plugin-catalog-backend'; +import { Events } from '@backstage/plugin-bitbucket-cloud-common'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import { + ANNOTATION_BITBUCKET_CLOUD_REPO_URL, + BitbucketCloudEntityProvider, +} from './BitbucketCloudEntityProvider'; class PersistingTaskRunner implements TaskRunner { private tasks: TaskInvocationDefinition[] = []; @@ -38,6 +47,10 @@ class PersistingTaskRunner implements TaskRunner { this.tasks.push(task); return Promise.resolve(undefined); } + + reset() { + this.tasks = []; + } } const logger = getVoidLogger(); @@ -46,10 +59,103 @@ const server = setupServer(); describe('BitbucketCloudEntityProvider', () => { setupRequestMockHandlers(server); - afterEach(() => jest.resetAllMocks()); + + const simpleConfig = new ConfigReader({ + catalog: { + providers: { + bitbucketCloud: { + workspace: 'test-ws', + }, + }, + }, + }); + const defaultConfig = new ConfigReader({ + catalog: { + providers: { + bitbucketCloud: { + myProvider: { + workspace: 'test-ws', + catalogPath: 'catalog-custom.yaml', + filters: { + projectKey: 'test-.*', + repoSlug: 'test-.*', + }, + }, + }, + }, + }, + }); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const tokenManager = { + getToken: async () => { + return { token: 'fake-token' }; + }, + } as any as TokenManager; + const repoPushEvent: Events.RepoPushEvent = { + actor: { + type: 'user', + }, + repository: { + type: 'repository', + slug: 'test-repo', + links: { + html: { + href: 'https://bitbucket.org/test-ws/test-repo', + }, + }, + workspace: { + type: 'workspace', + slug: 'test-ws', + }, + project: { + type: 'project', + key: 'test-project', + }, + }, + push: { + changes: [ + // ... + ], + }, + }; + const repoPushEventParams = { + topic: 'bitbucketCloud.repo:push', + eventPayload: repoPushEvent, + metadata: { 'x-event-key': 'repo:push' }, + }; + + const createLocationEntity = ( + repoUrl: string, + branch: string, + targetPath: string, + ): LocationEntity => { + const target = `${repoUrl}/src/${branch}/${targetPath}`; + + const entity = locationSpecToLocationEntity({ + location: { + type: 'url', + target: target, + presence: 'required', + }, + }); + entity.metadata.annotations = { + ...entity.metadata.annotations, + [ANNOTATION_BITBUCKET_CLOUD_REPO_URL]: repoUrl, + }; + + return entity; + }; + + afterEach(() => { + jest.resetAllMocks(); + schedule.reset(); + }); it('no provider config', () => { - const schedule = new PersistingTaskRunner(); const config = new ConfigReader({}); const providers = BitbucketCloudEntityProvider.fromConfig(config, { logger, @@ -60,17 +166,7 @@ describe('BitbucketCloudEntityProvider', () => { }); it('single simple provider config', () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - bitbucketCloud: { - workspace: 'test-ws', - }, - }, - }, - }); - const providers = BitbucketCloudEntityProvider.fromConfig(config, { + const providers = BitbucketCloudEntityProvider.fromConfig(simpleConfig, { logger, schedule, }); @@ -82,18 +178,8 @@ describe('BitbucketCloudEntityProvider', () => { }); it('fail without schedule and scheduler', () => { - const config = new ConfigReader({ - catalog: { - providers: { - bitbucketCloud: { - workspace: 'test-ws', - }, - }, - }, - }); - expect(() => - BitbucketCloudEntityProvider.fromConfig(config, { + BitbucketCloudEntityProvider.fromConfig(simpleConfig, { logger, }), ).toThrow('Either schedule or scheduler must be provided.'); @@ -151,7 +237,6 @@ describe('BitbucketCloudEntityProvider', () => { }); it('multiple provider configs', () => { - const schedule = new PersistingTaskRunner(); const config = new ConfigReader({ catalog: { providers: { @@ -181,28 +266,7 @@ describe('BitbucketCloudEntityProvider', () => { }); it('apply full update on scheduled execution', async () => { - const config = new ConfigReader({ - catalog: { - providers: { - bitbucketCloud: { - myProvider: { - workspace: 'test-ws', - catalogPath: 'custom/path/catalog-custom.yaml', - filters: { - projectKey: 'test-.*', - repoSlug: 'test-.*', - }, - }, - }, - }, - }, - }); - const schedule = new PersistingTaskRunner(); - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - const provider = BitbucketCloudEntityProvider.fromConfig(config, { + const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { logger, schedule, })[0]; @@ -333,6 +397,8 @@ describe('BitbucketCloudEntityProvider', () => { annotations: { 'backstage.io/managed-by-location': `url:${url}`, 'backstage.io/managed-by-origin-location': `url:${url}`, + 'bitbucket.org/repo-url': + 'https://bitbucket.org/test-ws/test-repo2', }, name: 'generated-7c2e6263b6cc2d14e69fd4d029afba601ad6dc3b', }, @@ -352,4 +418,221 @@ describe('BitbucketCloudEntityProvider', () => { entities: expectedEntities, }); }); + + it('update onRepoPush', async () => { + const keptModule = createLocationEntity( + 'https://bitbucket.org/test-ws/test-repo', + 'main', + 'kept-module/catalog-custom.yaml', + ); + const removedModule = createLocationEntity( + 'https://bitbucket.org/test-ws/test-repo', + 'main', + 'removed-module/catalog-custom.yaml', + ); + const addedModule = createLocationEntity( + 'https://bitbucket.org/test-ws/test-repo', + 'main', + 'added-module/catalog-custom.yaml', + ); + + const catalogApi = { + getEntities: async ( + request: { filter: Record }, + options: { token: string }, + ): Promise<{ items: Entity[] }> => { + if ( + options.token !== 'fake-token' || + request.filter.kind !== 'Location' || + request.filter['metadata.annotations.bitbucket.org/repo-url'] !== + 'https://bitbucket.org/test-ws/test-repo' + ) { + return { items: [] }; + } + + return { + items: [keptModule, removedModule], + }; + }, + }; + const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + tokenManager, + })[0]; + + server.use( + rest.get( + `https://api.bitbucket.org/2.0/workspaces/test-ws/search/code`, + (req, res, ctx) => { + const query = req.url.searchParams.get('search_query'); + if (!query || !query.includes('repo:test-repo')) { + return res(ctx.json({ values: [] })); + } + + const response = { + values: [ + { + path_matches: [ + { + match: true, + text: 'catalog-custom.yaml', + }, + ], + file: { + type: 'commit_file', + path: 'kept-module/catalog-custom.yaml', + commit: { + repository: { + slug: 'test-repo', + project: { + key: 'test-project', + }, + mainbranch: { + name: 'main', + }, + links: { + html: { + href: 'https://bitbucket.org/test-ws/test-repo', + }, + }, + }, + }, + }, + }, + { + path_matches: [ + { + match: true, + text: 'catalog-custom.yaml', + }, + ], + file: { + type: 'commit_file', + path: 'added-module/catalog-custom.yaml', + commit: { + repository: { + slug: 'test-repo', + project: { + key: 'test-project', + }, + mainbranch: { + name: 'main', + }, + links: { + html: { + href: 'https://bitbucket.org/test-ws/test-repo', + }, + }, + }, + }, + }, + }, + ], + }; + return res(ctx.json(response)); + }, + ), + ); + + await provider.connect(entityProviderConnection); + await provider.onEvent(repoPushEventParams); + + const addedEntities = [ + { + entity: addedModule, + locationKey: 'bitbucketCloud-provider:myProvider', + }, + ]; + const removedEntities = [ + { + entity: removedModule, + locationKey: 'bitbucketCloud-provider:myProvider', + }, + ]; + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ + keys: [ + 'url:https://bitbucket.org/test-ws/test-repo/src/main/kept-module/catalog-custom.yaml', + ], + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: addedEntities, + removed: removedEntities, + }); + }); + + it('onRepoPush fail on incomplete setup', async () => { + const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + logger, + schedule, + })[0]; + + await expect(provider.onEvent(repoPushEventParams)).rejects.toThrow( + 'bitbucketCloud-provider:myProvider not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.', + ); + }); + + it('no onRepoPush update on non-matching workspace slug', async () => { + const catalogApi = { + getEntities: jest.fn(), + refreshEntity: jest.fn(), + }; + const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await provider.onEvent({ + ...repoPushEventParams, + eventPayload: { + ...repoPushEventParams.eventPayload, + repository: { + ...repoPushEventParams.eventPayload.repository, + workspace: { + ...repoPushEventParams.eventPayload.repository.workspace, + slug: 'not-matching', + }, + }, + }, + }); + + expect(catalogApi.refreshEntity).toHaveBeenCalledTimes(0); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); + + it('no onRepoPush update on non-matching repo slug', async () => { + const catalogApi = { + getEntities: jest.fn(), + refreshEntity: jest.fn(), + }; + const provider = BitbucketCloudEntityProvider.fromConfig(defaultConfig, { + catalogApi: catalogApi as any as CatalogApi, + logger, + schedule, + tokenManager, + })[0]; + + await provider.connect(entityProviderConnection); + await provider.onEvent({ + ...repoPushEventParams, + eventPayload: { + ...repoPushEventParams.eventPayload, + repository: { + ...repoPushEventParams.eventPayload.repository, + slug: 'not-matching', + }, + }, + }); + + expect(catalogApi.refreshEntity).toHaveBeenCalledTimes(0); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); }); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts index f3334a6574..83b6f447f2 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/BitbucketCloudEntityProvider.ts @@ -14,7 +14,10 @@ * limitations under the License. */ +import { TokenManager } from '@backstage/backend-common'; import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; +import { CatalogApi } from '@backstage/catalog-client'; +import { LocationEntity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { BitbucketCloudIntegration, @@ -22,14 +25,17 @@ import { } from '@backstage/integration'; import { BitbucketCloudClient, + Events, Models, } from '@backstage/plugin-bitbucket-cloud-common'; import { + DeferredEntity, EntityProvider, EntityProviderConnection, - LocationSpec, locationSpecToLocationEntity, } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; +import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; import { BitbucketCloudEntityProviderConfig, readProviderConfigs, @@ -38,6 +44,15 @@ import * as uuid from 'uuid'; import { Logger } from 'winston'; const DEFAULT_BRANCH = 'master'; +const TOPIC_REPO_PUSH = 'bitbucketCloud.repo:push'; + +/** @public */ +export const ANNOTATION_BITBUCKET_CLOUD_REPO_URL = 'bitbucket.org/repo-url'; + +interface IngestionTarget { + fileUrl: string; + repoUrl: string; +} /** * Discovers catalog files located in [Bitbucket Cloud](https://bitbucket.org). @@ -47,19 +62,27 @@ const DEFAULT_BRANCH = 'master'; * * @public */ -export class BitbucketCloudEntityProvider implements EntityProvider { +export class BitbucketCloudEntityProvider + implements EntityProvider, EventSubscriber +{ private readonly client: BitbucketCloudClient; private readonly config: BitbucketCloudEntityProviderConfig; private readonly logger: Logger; private readonly scheduleFn: () => Promise; + private readonly catalogApi?: CatalogApi; + private readonly tokenManager?: TokenManager; private connection?: EntityProviderConnection; + private eventConfigErrorThrown = false; + static fromConfig( config: Config, options: { + catalogApi?: CatalogApi; logger: Logger; schedule?: TaskRunner; scheduler?: PluginTaskScheduler; + tokenManager?: TokenManager; }, ): BitbucketCloudEntityProvider[] { const integrations = ScmIntegrations.fromConfig(config); @@ -90,6 +113,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider { integration, options.logger, taskRunner, + options.catalogApi, + options.tokenManager, ); }); } @@ -99,6 +124,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider { integration: BitbucketCloudIntegration, logger: Logger, taskRunner: TaskRunner, + catalogApi?: CatalogApi, + tokenManager?: TokenManager, ) { this.client = BitbucketCloudClient.fromConfig(integration.config); this.config = config; @@ -106,6 +133,8 @@ export class BitbucketCloudEntityProvider implements EntityProvider { target: this.getProviderName(), }); this.scheduleFn = this.createScheduleFn(taskRunner); + this.catalogApi = catalogApi; + this.tokenManager = tokenManager; } private createScheduleFn(schedule: TaskRunner): () => Promise { @@ -154,15 +183,7 @@ export class BitbucketCloudEntityProvider implements EntityProvider { logger.info('Discovering catalog files in Bitbucket Cloud repositories'); const targets = await this.findCatalogFiles(); - const entities = targets - .map(BitbucketCloudEntityProvider.toLocationSpec) - .map(location => locationSpecToLocationEntity({ location })) - .map(entity => { - return { - locationKey: this.getProviderName(), - entity: entity, - }; - }); + const entities = this.toDeferredEntities(targets); await this.connection.applyMutation({ type: 'full', @@ -174,7 +195,129 @@ export class BitbucketCloudEntityProvider implements EntityProvider { ); } - private async findCatalogFiles(): Promise { + /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.supportsEventTopics} */ + supportsEventTopics(): string[] { + return [TOPIC_REPO_PUSH]; + } + + /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.onEvent} */ + async onEvent(params: EventParams): Promise { + if (params.topic !== TOPIC_REPO_PUSH) { + return; + } + + await this.onRepoPush(params.eventPayload as Events.RepoPushEvent); + } + + private canHandleEvents(): boolean { + if (this.catalogApi && this.tokenManager) { + return true; + } + + // throw only once + if (!this.eventConfigErrorThrown) { + this.eventConfigErrorThrown = true; + throw new Error( + `${this.getProviderName()} not well configured to handle repo:push. Missing CatalogApi and/or TokenManager.`, + ); + } + + return false; + } + + async onRepoPush(event: Events.RepoPushEvent): Promise { + if (!this.canHandleEvents()) { + return; + } + + if (!this.connection) { + throw new Error('Not initialized'); + } + + if (event.repository.workspace.slug !== this.config.workspace) { + return; + } + + if (!this.matchesFilters(event.repository)) { + return; + } + + const repoName = event.repository.slug; + const repoUrl = event.repository.links!.html!.href!; + this.logger.info(`handle repo:push event for ${repoUrl}`); + + // The commit information at the webhook only contains some high level metadata. + // In order to understand whether relevant files have changed we would need to + // look up all commits which would cost additional API calls. + // The overall goal is to optimize the necessary amount of API calls. + // Hence, we will just trigger a refresh for catalog file(s) within the repository + // if we get notified about changes there. + + const targets = await this.findCatalogFiles(repoName); + + const { token } = await this.tokenManager!.getToken(); + const existing = await this.findExistingLocations(repoUrl, token); + + const added: DeferredEntity[] = this.toDeferredEntities( + targets.filter( + // All Locations are managed by this provider and only have `target`, never `targets`. + // All URLs (fileUrl, target) are created using `BitbucketCloudEntityProvider.toUrl`. + // Hence, we can keep the comparison simple and don't need to handle different + // casing or encoding, etc. + target => !existing.find(item => item.spec.target === target.fileUrl), + ), + ); + + const stillExisting: LocationEntity[] = []; + const removed: DeferredEntity[] = []; + existing.forEach(item => { + if (targets.find(value => value.fileUrl === item.spec.target)) { + stillExisting.push(item); + } else { + removed.push({ + locationKey: this.getProviderName(), + entity: item, + }); + } + }); + + const promises: Promise[] = [ + this.connection.refresh({ + keys: stillExisting.map(entity => `url:${entity.spec.target}`), + }), + ]; + + if (added.length > 0 || removed.length > 0) { + const connection = this.connection; + promises.push( + connection.applyMutation({ + type: 'delta', + added: added, + removed: removed, + }), + ); + } + + await Promise.all(promises); + } + + private async findExistingLocations( + repoUrl: string, + token: string, + ): Promise { + const filter: Record = {}; + filter.kind = 'Location'; + filter[`metadata.annotations.${ANNOTATION_BITBUCKET_CLOUD_REPO_URL}`] = + repoUrl; + + return this.catalogApi!.getEntities({ filter }, { token }).then( + result => result.items, + ) as Promise; + } + + private async findCatalogFiles( + repoName?: string, + ): Promise { const workspace = this.config.workspace; const catalogPath = this.config.catalogPath; @@ -197,12 +340,13 @@ export class BitbucketCloudEntityProvider implements EntityProvider { // ...except the one we need '+values.file.commit.repository.links.html.href', ].join(','); - const query = `"${catalogFilename}" path:${catalogPath}`; + const optRepoFilter = repoName ? ` repo:${repoName}` : ''; + const query = `"${catalogFilename}" path:${catalogPath}${optRepoFilter}`; const searchResults = this.client .searchCode(workspace, query, { fields }) .iterateResults(); - const result: string[] = []; + const result: IngestionTarget[] = []; for await (const searchResult of searchResults) { // not a file match, but a code match @@ -212,12 +356,13 @@ export class BitbucketCloudEntityProvider implements EntityProvider { const repository = searchResult.file!.commit!.repository!; if (this.matchesFilters(repository)) { - result.push( - BitbucketCloudEntityProvider.toUrl( + result.push({ + fileUrl: BitbucketCloudEntityProvider.toUrl( repository, searchResult.file!.path!, ), - ); + repoUrl: repository.links!.html!.href!, + }); } } @@ -234,11 +379,32 @@ export class BitbucketCloudEntityProvider implements EntityProvider { ); } + private toDeferredEntities(targets: IngestionTarget[]): DeferredEntity[] { + return targets + .map(target => { + const location = BitbucketCloudEntityProvider.toLocationSpec( + target.fileUrl, + ); + const entity = locationSpecToLocationEntity({ location }); + entity.metadata.annotations = { + ...entity.metadata.annotations, + [ANNOTATION_BITBUCKET_CLOUD_REPO_URL]: target.repoUrl, + }; + return entity; + }) + .map(entity => { + return { + locationKey: this.getProviderName(), + entity: entity, + }; + }); + } + private static toUrl( repository: Models.Repository, filePath: string, ): string { - const repoUrl = repository.links!.html!.href; + const repoUrl = repository.links!.html!.href!; const branch = repository.mainbranch?.name ?? DEFAULT_BRANCH; return `${repoUrl}/src/${branch}/${filePath}`; diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudCatalogModule.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.test.ts similarity index 70% rename from plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudCatalogModule.test.ts rename to plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.test.ts index 6307bee282..83e657bcfe 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudCatalogModule.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.test.ts @@ -15,32 +15,39 @@ */ import { ConfigReader } from '@backstage/config'; -import { getVoidLogger } from '@backstage/backend-common'; import { - configServiceRef, - loggerServiceRef, - schedulerServiceRef, -} from '@backstage/backend-plugin-api'; + getVoidLogger, + PluginEndpointDiscovery, + TokenManager, +} from '@backstage/backend-common'; +import { coreServices } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskScheduleDefinition, } from '@backstage/backend-tasks'; import { startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; -import { bitbucketCloudEntityProviderCatalogModule } from './BitbucketCloudEntityProviderCatalogModule'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; import { Duration } from 'luxon'; +import { bitbucketCloudEntityProviderCatalogModule } from './BitbucketCloudEntityProviderCatalogModule'; import { BitbucketCloudEntityProvider } from '../BitbucketCloudEntityProvider'; describe('bitbucketCloudEntityProviderCatalogModule', () => { it('should register provider at the catalog extension point', async () => { let addedProviders: Array | undefined; + let addedSubscribers: Array | undefined; let usedSchedule: TaskScheduleDefinition | undefined; - const extensionPoint = { + const catalogExtensionPointImpl = { addEntityProvider: (providers: any) => { addedProviders = providers; }, }; + const eventsExtensionPointImpl = { + addSubscribers: (subscribers: any) => { + addedSubscribers = subscribers; + }, + }; const runner = jest.fn(); const scheduler = { createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { @@ -48,6 +55,8 @@ describe('bitbucketCloudEntityProviderCatalogModule', () => { return runner; }, } as unknown as PluginTaskScheduler; + const discovery = jest.fn() as any as PluginEndpointDiscovery; + const tokenManager = jest.fn() as any as TokenManager; const config = new ConfigReader({ catalog: { @@ -64,11 +73,16 @@ describe('bitbucketCloudEntityProviderCatalogModule', () => { }); await startTestBackend({ - extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], + extensionPoints: [ + [catalogProcessingExtensionPoint, catalogExtensionPointImpl], + [eventsExtensionPoint, eventsExtensionPointImpl], + ], services: [ - [configServiceRef, config], - [loggerServiceRef, getVoidLogger()], - [schedulerServiceRef, scheduler], + [coreServices.config, config], + [coreServices.discovery, discovery], + [coreServices.logger, getVoidLogger()], + [coreServices.scheduler, scheduler], + [coreServices.tokenManager, tokenManager], ], features: [bitbucketCloudEntityProviderCatalogModule()], }); @@ -79,6 +93,7 @@ describe('bitbucketCloudEntityProviderCatalogModule', () => { expect(addedProviders?.pop()?.getProviderName()).toEqual( 'bitbucketCloud-provider:default', ); + expect(addedSubscribers).toEqual(addedProviders); expect(runner).not.toHaveBeenCalled(); }); }); diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.ts index d137bd7166..19b83b92b6 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/service/BitbucketCloudEntityProviderCatalogModule.ts @@ -15,13 +15,15 @@ */ import { - configServiceRef, + coreServices, createBackendModule, - loggerServiceRef, loggerToWinstonLogger, - schedulerServiceRef, } from '@backstage/backend-plugin-api'; -import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { + catalogProcessingExtensionPoint, + catalogServiceRef, +} from '@backstage/plugin-catalog-node'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; import { BitbucketCloudEntityProvider } from '../BitbucketCloudEntityProvider'; /** @@ -34,18 +36,34 @@ export const bitbucketCloudEntityProviderCatalogModule = createBackendModule({ env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, - config: configServiceRef, - logger: loggerServiceRef, - scheduler: schedulerServiceRef, + catalogApi: catalogServiceRef, + config: coreServices.config, + // TODO(pjungermann): How to make this optional for those which only want the provider without event support? + // Do we even want to support this? + events: eventsExtensionPoint, + logger: coreServices.logger, + scheduler: coreServices.scheduler, + tokenManager: coreServices.tokenManager, }, - async init({ catalog, config, logger, scheduler }) { + async init({ + catalog, + catalogApi, + config, + events, + logger, + scheduler, + tokenManager, + }) { const winstonLogger = loggerToWinstonLogger(logger); const providers = BitbucketCloudEntityProvider.fromConfig(config, { + catalogApi, logger: winstonLogger, scheduler, + tokenManager, }); catalog.addEntityProvider(providers); + events.addSubscribers(providers); }, }); }, diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index 1224da8bdd..0ce053d1df 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.1.3 + +### Patch Changes + +- 68f7f5a857: `BitbucketServerEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + https://backstage.io/docs/integrations/bitbucketServer/discovery + +- cd48ed8370: Add `bitbucketServerEntityProviderCatalogModule` (new backend-plugin-api, alpha). +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + ## 0.1.3-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index 594b5f0852..deddc7ec66 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.3-next.0", + "version": "0.1.4-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -50,7 +50,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "luxon": "^3.0.0", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "alpha", diff --git a/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.test.ts index d2ce4dbd3d..474431dc7a 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.test.ts @@ -16,11 +16,7 @@ import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; -import { - configServiceRef, - loggerServiceRef, - schedulerServiceRef, -} from '@backstage/backend-plugin-api'; +import { coreServices } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskScheduleDefinition, @@ -73,9 +69,9 @@ describe('bitbucketServerEntityProviderCatalogModule', () => { await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], services: [ - [configServiceRef, config], - [loggerServiceRef, getVoidLogger()], - [schedulerServiceRef, scheduler], + [coreServices.config, config], + [coreServices.logger, getVoidLogger()], + [coreServices.scheduler, scheduler], ], features: [bitbucketServerEntityProviderCatalogModule()], }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.ts index e89eee7b45..e0f5a21ff9 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/service/BitbucketServerEntityProviderCatalogModule.ts @@ -15,11 +15,9 @@ */ import { - configServiceRef, + coreServices, createBackendModule, - loggerServiceRef, loggerToWinstonLogger, - schedulerServiceRef, } from '@backstage/backend-plugin-api'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; import { BitbucketServerEntityProvider } from '../providers'; @@ -34,9 +32,9 @@ export const bitbucketServerEntityProviderCatalogModule = createBackendModule({ env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, - config: configServiceRef, - logger: loggerServiceRef, - scheduler: schedulerServiceRef, + config: coreServices.config, + logger: coreServices.logger, + scheduler: coreServices.scheduler, }, async init({ catalog, config, logger, scheduler }) { const winstonLogger = loggerToWinstonLogger(logger); diff --git a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md index 571c18de00..bc0e0902bf 100644 --- a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-catalog-backend-module-bitbucket +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.2-next.1 + +## 0.2.6-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.2-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.2.5 + +### Patch Changes + +- 4c9f7847e4: Updated dependency `msw` to `^0.48.0` while moving it to be a dev dependency. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.1-next.0 + ## 0.2.5-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket/package.json b/plugins/catalog-backend-module-bitbucket/package.json index 8ee6661b01..6f660ce3f5 100644 --- a/plugins/catalog-backend-module-bitbucket/package.json +++ b/plugins/catalog-backend-module-bitbucket/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket", - "version": "0.2.5-next.0", + "version": "0.2.6-next.1", "deprecated": true, "main": "src/index.ts", "types": "src/index.ts", @@ -42,14 +42,14 @@ "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/types": "workspace:^", "lodash": "^4.17.21", - "msw": "^0.47.0", "node-fetch": "^2.6.7", "winston": "^3.2.1" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", - "@types/lodash": "^4.14.151" + "@types/lodash": "^4.14.151", + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index a6babaf4fc..e0f334d57c 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,81 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.1.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.1.6 + +### Patch Changes + +- 4fba50f5d4: Add `gerritEntityProviderCatalogModule` (new backend-plugin-api, alpha). +- 4c9f7847e4: Updated dependency `msw` to `^0.48.0` while moving it to be a dev dependency. +- 134b69f478: `GerritEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + https://backstage.io/docs/integrations/gerrit/discovery + +- a6d779d58a: Remove explicit default visibility at `config.d.ts` files. + + ```ts + /** + * @visibility backend + */ + ``` + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + ## 0.1.6-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index c6d342640f..5ebc8481c1 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.6-next.0", + "version": "0.1.7-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,7 +39,6 @@ "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "fs-extra": "10.1.0", - "msw": "^0.47.0", "node-fetch": "^2.6.7", "uuid": "^8.0.0", "winston": "^3.2.1" @@ -48,7 +47,8 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/fs-extra": "^9.0.1", - "luxon": "^3.0.0" + "luxon": "^3.0.0", + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.test.ts index 53e012209b..9d61cabdb9 100644 --- a/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.test.ts @@ -15,11 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { - configServiceRef, - loggerServiceRef, - schedulerServiceRef, -} from '@backstage/backend-plugin-api'; +import { coreServices } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskScheduleDefinition, @@ -79,9 +75,9 @@ describe('gerritEntityProviderCatalogModule', () => { await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], services: [ - [configServiceRef, config], - [loggerServiceRef, getVoidLogger()], - [schedulerServiceRef, scheduler], + [coreServices.config, config], + [coreServices.logger, getVoidLogger()], + [coreServices.scheduler, scheduler], ], features: [gerritEntityProviderCatalogModule()], }); diff --git a/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.ts index 32d582e397..674845862f 100644 --- a/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-gerrit/src/service/GerritEntityProviderCatalogModule.ts @@ -15,11 +15,9 @@ */ import { - configServiceRef, + coreServices, createBackendModule, - loggerServiceRef, loggerToWinstonLogger, - schedulerServiceRef, } from '@backstage/backend-plugin-api'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; import { GerritEntityProvider } from '../providers/GerritEntityProvider'; @@ -34,9 +32,9 @@ export const gerritEntityProviderCatalogModule = createBackendModule({ env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, - config: configServiceRef, - logger: loggerServiceRef, - scheduler: schedulerServiceRef, + config: coreServices.config, + logger: coreServices.logger, + scheduler: coreServices.scheduler, }, async init({ catalog, config, logger, scheduler }) { const winstonLogger = loggerToWinstonLogger(logger); diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index b89231a422..de031b4466 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,121 @@ # @backstage/plugin-catalog-backend-module-github +## 0.2.2-next.1 + +### Patch Changes + +- fe93cce743: Added the githubCredentialsProvider property to the GithubLocationAnalyzerOptions to be able to override the GithubCredentialsProvider. +- a0fd4af94a: Handle GitHub `push` events at the `GithubEntityProvider` by subscribing to the topic `github.push.` + + Implements `EventSubscriber` to receive events for the topic `github.push`. + + On `github.push`, the affected repository will be refreshed. + This includes adding new Location entities, refreshing existing ones, + and removing obsolete ones. + + Please find more information at + https://backstage.io/docs/integrations/github/discovery#installation-with-events-support + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- 70fa5ec3ec: Fixes the assignment of group member references in `GithubMultiOrgProcessor` so membership relations are resolved correctly. +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 754b5854df: Fix incorrectly exported GithubOrgEntityProvider as a type +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.2.0 + +### Minor Changes + +- 67fe5bc9a9: BREAKING: Support authenticated backends by including a server token for catalog requests. The constructor of `GithubLocationAnalyzer` now requires an instance of `TokenManager` to be supplied: + + ```diff + ... + builder.addLocationAnalyzers( + new GitHubLocationAnalyzer({ + discovery: env.discovery, + config: env.config, + + tokenManager: env.tokenManager, + }), + ); + ... + ``` + +- f64d66a45c: Added the ability for the GitHub discovery provider to validate that catalog files exist before emitting them. + + Users can now set the `validateLocationsExist` property to `true` in their GitHub discovery configuration to opt in to this feature. + This feature only works with `catalogPath`s that do not contain wildcards. + + When `validateLocationsExist` is set to `true`, the GitHub discovery provider will retrieve the object from the + repository at the provided `catalogPath`. + If this file exists and is non-empty, then it will be emitted as a location for further processing. + If this file does not exist or is empty, then it will not be emitted. + Not emitting locations that do not exist allows for far fewer calls to the GitHub API to validate locations that do not exist. + +### Patch Changes + +- 67fe5bc9a9: Properly derive Github credentials when making requests in `GithubLocationAnalyzer` to support Github App authentication +- bef063dc8d: - Make it possible to inject custom user and team transformers when configuring the `GithubOrgEntityProvider` +- 4c9f7847e4: Updated dependency `msw` to `^0.48.0` while moving it to be a dev dependency. +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + ## 0.2.0-next.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-github/api-report.md b/plugins/catalog-backend-module-github/api-report.md index a9587e172b..8ccfdb09ee 100644 --- a/plugins/catalog-backend-module-github/api-report.md +++ b/plugins/catalog-backend-module-github/api-report.md @@ -11,8 +11,12 @@ import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-backend'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; +import { EventParams } from '@backstage/plugin-events-node'; +import { EventSubscriber } from '@backstage/plugin-events-node'; import { GithubCredentialsProvider } from '@backstage/integration'; import { GithubIntegrationConfig } from '@backstage/integration'; +import { graphql } from '@octokit/graphql'; +import { GroupEntity } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -21,6 +25,13 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { ScmLocationAnalyzer } from '@backstage/plugin-catalog-backend'; import { TaskRunner } from '@backstage/backend-tasks'; import { TokenManager } from '@backstage/backend-common'; +import { UserEntity } from '@backstage/catalog-model'; + +// @public +export const defaultOrganizationTeamTransformer: TeamTransformer; + +// @public +export const defaultUserTransformer: UserTransformer; // @public export class GithubDiscoveryProcessor implements CatalogProcessor { @@ -67,7 +78,7 @@ export class GitHubEntityProvider implements EntityProvider { } // @public -export class GithubEntityProvider implements EntityProvider { +export class GithubEntityProvider implements EntityProvider, EventSubscriber { // (undocumented) connect(connection: EntityProviderConnection): Promise; // (undocumented) @@ -82,7 +93,11 @@ export class GithubEntityProvider implements EntityProvider { // (undocumented) getProviderName(): string; // (undocumented) + onEvent(params: EventParams): Promise; + // (undocumented) refresh(logger: Logger): Promise; + // (undocumented) + supportsEventTopics(): string[]; } // @alpha @@ -113,6 +128,7 @@ export type GithubLocationAnalyzerOptions = { config: Config; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; + githubCredentialsProvider?: GithubCredentialsProvider; }; // @public @@ -129,6 +145,8 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { logger: Logger; orgs: GithubMultiOrgConfig; githubCredentialsProvider?: GithubCredentialsProvider; + userTransformer?: UserTransformer; + teamTransformer?: TeamTransformer; }); // (undocumented) static fromConfig( @@ -136,6 +154,8 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { options: { logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; + userTransformer?: UserTransformer; + teamTransformer?: TeamTransformer; }, ): GithubMultiOrgReaderProcessor; // (undocumented) @@ -165,6 +185,8 @@ export class GithubOrgEntityProvider implements EntityProvider { gitHubConfig: GithubIntegrationConfig; logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; + userTransformer?: UserTransformer; + teamTransformer?: TeamTransformer; }); // (undocumented) connect(connection: EntityProviderConnection): Promise; @@ -188,6 +210,8 @@ export interface GithubOrgEntityProviderOptions { logger: Logger; orgUrl: string; schedule?: 'manual' | TaskRunner; + teamTransformer?: TeamTransformer; + userTransformer?: UserTransformer; } // @public @@ -214,4 +238,48 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { emit: CatalogProcessorEmit, ): Promise; } + +// @public +export type GithubTeam = { + slug: string; + combinedSlug: string; + name?: string; + description?: string; + avatarUrl?: string; + editTeamUrl?: string; + parentTeam?: GithubTeam; + members: GithubUser[]; +}; + +// @public +export type GithubUser = { + login: string; + bio?: string; + avatarUrl?: string; + email?: string; + name?: string; + organizationVerifiedDomainEmails?: string[]; +}; + +// @public +export type TeamTransformer = ( + item: GithubTeam, + ctx: TransformerContext, +) => Promise; + +// @public +export interface TransformerContext { + // (undocumented) + client: typeof graphql; + // (undocumented) + org: string; + // (undocumented) + query: string; +} + +// @public +export type UserTransformer = ( + item: GithubUser, + ctx: TransformerContext, +) => Promise; ``` diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index 60185636ea..15910c030a 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-github", "description": "A Backstage catalog backend module that helps integrate towards GitHub", - "version": "0.2.0-next.0", + "version": "0.2.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,13 +42,14 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", "@backstage/types": "workspace:^", "@octokit/graphql": "^5.0.0", "@octokit/rest": "^19.0.3", "git-url-parse": "^13.0.0", "lodash": "^4.17.21", - "msw": "^0.47.0", "node-fetch": "^2.6.7", "uuid": "^8.0.0", "winston": "^3.2.1" @@ -57,7 +58,8 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", - "luxon": "^3.0.0" + "luxon": "^3.0.0", + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.ts b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.ts index a740fd7706..139a92ee83 100644 --- a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.ts +++ b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.ts @@ -39,6 +39,7 @@ export type GithubLocationAnalyzerOptions = { config: Config; discovery: PluginEndpointDiscovery; tokenManager: TokenManager; + githubCredentialsProvider?: GithubCredentialsProvider; }; /** @public */ @@ -52,6 +53,7 @@ export class GithubLocationAnalyzer implements ScmLocationAnalyzer { this.catalogClient = new CatalogClient({ discoveryApi: options.discovery }); this.integrations = ScmIntegrations.fromConfig(options.config); this.githubCredentialsProvider = + options.githubCredentialsProvider || DefaultGithubCredentialsProvider.fromIntegrations(this.integrations); this.tokenManager = options.tokenManager; } diff --git a/plugins/catalog-backend-module-github/src/index.ts b/plugins/catalog-backend-module-github/src/index.ts index f38035db15..b005780758 100644 --- a/plugins/catalog-backend-module-github/src/index.ts +++ b/plugins/catalog-backend-module-github/src/index.ts @@ -15,14 +15,13 @@ */ /** - * A Backstage catalog backend module that helps integrate towards GitHub + * A Backstage catalog backend module that helps integrate towards Github * * @packageDocumentation */ export { GithubLocationAnalyzer } from './analyzers/GithubLocationAnalyzer'; export type { GithubLocationAnalyzerOptions } from './analyzers/GithubLocationAnalyzer'; -export type { GithubMultiOrgConfig } from './lib'; export { GithubDiscoveryProcessor } from './processors/GithubDiscoveryProcessor'; export { GithubMultiOrgReaderProcessor } from './processors/GithubMultiOrgReaderProcessor'; export { GithubOrgReaderProcessor } from './processors/GithubOrgReaderProcessor'; @@ -30,5 +29,15 @@ export { GithubEntityProvider } from './providers/GithubEntityProvider'; export { GithubOrgEntityProvider } from './providers/GithubOrgEntityProvider'; export type { GithubOrgEntityProviderOptions } from './providers/GithubOrgEntityProvider'; export { githubEntityProviderCatalogModule } from './service/GithubEntityProviderCatalogModule'; +export { + type GithubMultiOrgConfig, + type GithubTeam, + type GithubUser, + type UserTransformer, + defaultUserTransformer, + type TeamTransformer, + defaultOrganizationTeamTransformer, + type TransformerContext, +} from './lib'; export * from './deprecated'; diff --git a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts new file mode 100644 index 0000000000..efd3eedd5d --- /dev/null +++ b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts @@ -0,0 +1,127 @@ +/* + * 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 { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { graphql } from '@octokit/graphql'; +import { GithubTeam, GithubUser } from './github'; + +/** + * Context passed to Transformers + * + * @public + */ +export interface TransformerContext { + client: typeof graphql; + query: string; + org: string; +} + +/** + * Transformer for GitHub users to UserEntity + * + * @public + */ +export type UserTransformer = ( + item: GithubUser, + ctx: TransformerContext, +) => Promise; + +/** + * Transformer for GitHub Team to GroupEntity + * + * @public + */ +export type TeamTransformer = ( + item: GithubTeam, + ctx: TransformerContext, +) => Promise; + +/** + * Default transformer for GitHub users to UserEntity + * + * @public + */ +export const defaultUserTransformer: UserTransformer = async ( + item: GithubUser, +) => { + const entity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: item.login, + annotations: { + 'github.com/user-login': item.login, + }, + }, + spec: { + profile: {}, + memberOf: [], + }, + }; + + if (item.bio) entity.metadata.description = item.bio; + if (item.name) entity.spec.profile!.displayName = item.name; + if (item.email) entity.spec.profile!.email = item.email; + if (item.avatarUrl) entity.spec.profile!.picture = item.avatarUrl; + return entity; +}; + +/** + * Default transformer for GitHub Team to GroupEntity + * + * @public + */ +export const defaultOrganizationTeamTransformer: TeamTransformer = + async team => { + const annotations: { [annotationName: string]: string } = { + 'github.com/team-slug': team.combinedSlug, + }; + + if (team.editTeamUrl) { + annotations['backstage.io/edit-url'] = team.editTeamUrl; + } + + const entity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: team.slug, + annotations, + }, + spec: { + type: 'team', + profile: {}, + children: [], + }, + }; + + if (team.description) { + entity.metadata.description = team.description; + } + if (team.name) { + entity.spec.profile!.displayName = team.name; + } + if (team.avatarUrl) { + entity.spec.profile!.picture = team.avatarUrl; + } + if (team.parentTeam) { + entity.spec.parent = team.parentTeam.slug; + } + + entity.spec.members = team.members.map(user => user.login); + + return entity; + }; diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index 25878c2005..d07d2f6af8 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -15,22 +15,32 @@ */ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import { graphql } from '@octokit/graphql'; import { graphql as graphqlMsw } from 'msw'; import { setupServer } from 'msw/node'; +import { TeamTransformer, UserTransformer } from './defaultTransformers'; + import { getOrganizationTeams, getOrganizationUsers, getTeamMembers, getOrganizationRepositories, QueryResponse, + GithubUser, + GithubTeam, } from './github'; +import fetch from 'node-fetch'; + +// Workaround for Node.js 18, where native fetch is available, but not yet picked up by msw +// TODO(Rugvip): remove once https://github.com/mswjs/msw/issues/1388 is resolved +(global as any).fetch = fetch; describe('github', () => { const server = setupServer(); setupRequestMockHandlers(server); - describe('getOrganizationUsers', () => { + describe('getOrganizationUsers using defaultUserMapper', () => { it('reads members', async () => { const input: QueryResponse = { organization: { @@ -71,7 +81,120 @@ describe('github', () => { }); }); - describe('getOrganizationTeams', () => { + describe('getOrganizationUsers using custom UserTransformer', () => { + const customUserTransformer: UserTransformer = async ( + item: GithubUser, + {}, + ) => { + if (item.login === 'aa') { + return undefined; + } + + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: `${item.login}-custom`, + annotations: { + 'github.com/user-login': item.login, + }, + }, + spec: { + profile: {}, + memberOf: [], + }, + } as UserEntity; + }; + + it('reads members', async () => { + const input: QueryResponse = { + organization: { + membersWithRole: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'a', + name: 'b', + bio: 'c', + email: 'd', + avatarUrl: 'e', + }, + ], + }, + }, + }; + + const output = { + users: [ + expect.objectContaining({ + metadata: expect.objectContaining({ + name: 'a-custom', + }), + }), + ], + }; + + server.use( + graphqlMsw.query('users', (_req, res, ctx) => res(ctx.data(input))), + ); + + await expect( + getOrganizationUsers(graphql, 'a', 'token', customUserTransformer), + ).resolves.toEqual(output); + }); + + it('reads members if undefined is returned from transformer', async () => { + const input: QueryResponse = { + organization: { + membersWithRole: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + login: 'a', + name: 'b', + bio: 'c', + email: 'd', + avatarUrl: 'e', + }, + { + login: 'aa', + name: 'bb', + bio: 'cc', + email: 'dd', + avatarUrl: 'ee', + }, + ], + }, + }, + }; + + const output = { + users: [ + expect.objectContaining({ + metadata: expect.objectContaining({ + name: 'a-custom', + }), + }), + ], + }; + + server.use( + graphqlMsw.query('users', (_req, res, ctx) => res(ctx.data(input))), + ); + + const users = await getOrganizationUsers( + graphql, + 'a', + 'token', + customUserTransformer, + ); + + expect(users.users).toHaveLength(1); + expect(users).toEqual(output); + }); + }); + + describe('getOrganizationTeams using default TeamTransformer', () => { let input: QueryResponse; beforeEach(() => { @@ -90,7 +213,7 @@ describe('github', () => { parentTeam: { slug: 'parent', combinedSlug: '', - members: { pageInfo: { hasNextPage: false }, nodes: [] }, + members: [], }, members: { pageInfo: { hasNextPage: false }, @@ -124,10 +247,10 @@ describe('github', () => { }, parent: 'parent', children: [], + members: ['user'], }, }), ], - groupMemberUsers: new Map([['team', ['user']]]), }; server.use( @@ -136,37 +259,192 @@ describe('github', () => { await expect(getOrganizationTeams(graphql, 'a')).resolves.toEqual(output); }); + }); - it('applies namespaces', async () => { + describe('getOrganizationTeams using custom TeamTransformer', () => { + let input: QueryResponse; + + const customTeamTransformer: TeamTransformer = async ( + item: GithubTeam, + {}, + ) => { + if (item.name === 'aa') { + return undefined; + } + + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: `${item.name}-custom`, + annotations: { + 'github.com/team-slug': 'blah/team', + 'backstage.io/edit-url': + 'http://example.com/orgs/blah/teams/team/edit', + }, + description: item.description, + }, + spec: { + type: 'team', + profile: { + displayName: `${item.name}-custom`, + picture: 'http://example.com/team.jpeg', + }, + parent: 'parent', + children: [], + members: ['user'], + }, + } as GroupEntity; + }; + + beforeEach(() => { + input = { + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'blah/team', + name: 'Team', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit', + parentTeam: { + slug: 'parent', + combinedSlug: '', + members: [], + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'user' }], + }, + }, + ], + }, + }, + }; + }); + + it('reads teams', async () => { const output = { groups: [ expect.objectContaining({ metadata: expect.objectContaining({ - name: 'team', - namespace: 'foo', + name: 'Team-custom', description: 'The one and only team', + annotations: { + 'github.com/team-slug': 'blah/team', + 'backstage.io/edit-url': + 'http://example.com/orgs/blah/teams/team/edit', + }, }), spec: { type: 'team', profile: { - displayName: 'Team', + displayName: 'Team-custom', picture: 'http://example.com/team.jpeg', }, parent: 'parent', children: [], + members: ['user'], }, }), ], - groupMemberUsers: new Map([['foo/team', ['user']]]), }; server.use( graphqlMsw.query('teams', (_req, res, ctx) => res(ctx.data(input))), ); - await expect(getOrganizationTeams(graphql, 'a', 'foo')).resolves.toEqual( - output, + await expect( + getOrganizationTeams(graphql, 'a', customTeamTransformer), + ).resolves.toEqual(output); + }); + + it('reads teams if undefined is returned', async () => { + input = { + organization: { + teams: { + pageInfo: { hasNextPage: false }, + nodes: [ + { + slug: 'team', + combinedSlug: 'blah/team', + name: 'Team', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit', + parentTeam: { + slug: 'parent', + combinedSlug: '', + members: [], + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'user' }], + }, + }, + { + slug: 'team', + combinedSlug: 'blah/team', + name: 'aa', + description: 'The one and only team', + avatarUrl: 'http://example.com/team.jpeg', + editTeamUrl: 'http://example.com/orgs/blah/teams/team/edit', + parentTeam: { + slug: 'parent', + combinedSlug: '', + members: [], + }, + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'user' }], + }, + }, + ], + }, + }, + }; + + const output = { + groups: [ + expect.objectContaining({ + metadata: expect.objectContaining({ + name: 'Team-custom', + description: 'The one and only team', + annotations: { + 'github.com/team-slug': 'blah/team', + 'backstage.io/edit-url': + 'http://example.com/orgs/blah/teams/team/edit', + }, + }), + spec: { + type: 'team', + profile: { + displayName: 'Team-custom', + picture: 'http://example.com/team.jpeg', + }, + parent: 'parent', + children: [], + members: ['user'], + }, + }), + ], + }; + + server.use( + graphqlMsw.query('teams', (_req, res, ctx) => res(ctx.data(input))), ); + + const teams = await getOrganizationTeams( + graphql, + 'a', + customTeamTransformer, + ); + + expect(teams.groups).toHaveLength(1); + expect(teams).toEqual(output); }); }); @@ -186,7 +464,7 @@ describe('github', () => { }; const output = { - members: ['user'], + members: [{ login: 'user' }], }; server.use( diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index ef6a7f817a..4d583d0730 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -17,19 +17,30 @@ import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import { GithubCredentialType } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; +import { + defaultOrganizationTeamTransformer, + defaultUserTransformer, + TeamTransformer, + TransformerContext, + UserTransformer, +} from './defaultTransformers'; // Graphql types export type QueryResponse = { - organization?: Organization; - repositoryOwner?: Organization | User; + organization?: OrganizationResponse; + repositoryOwner?: RepositoryOwnerResponse; }; -export type Organization = { - membersWithRole?: Connection; - team?: Team; - teams?: Connection; - repositories?: Connection; +type RepositoryOwnerResponse = { + repositories?: Connection; +}; + +export type OrganizationResponse = { + membersWithRole?: Connection; + team?: GithubTeamResponse; + teams?: Connection; + repositories?: Connection; }; export type PageInfo = { @@ -37,27 +48,41 @@ export type PageInfo = { endCursor?: string; }; -export type User = { +/** + * Github User + * + * @public + */ +export type GithubUser = { login: string; bio?: string; avatarUrl?: string; email?: string; name?: string; - repositories?: Connection; + organizationVerifiedDomainEmails?: string[]; }; -export type Team = { +/** + * Github Team + * + * @public + */ +export type GithubTeam = { slug: string; combinedSlug: string; name?: string; description?: string; avatarUrl?: string; editTeamUrl?: string; - parentTeam?: Team; - members: Connection; + parentTeam?: GithubTeam; + members: GithubUser[]; }; -export type Repository = { +export type GithubTeamResponse = Omit & { + members: Connection; +}; + +export type RepositoryResponse = { name: string; url: string; isArchived: boolean; @@ -88,7 +113,7 @@ export type Connection = { }; /** - * Gets all the users out of a GitHub organization. + * Gets all the users out of a Github organization. * * Note that the users will not have their memberships filled in. * @@ -99,7 +124,7 @@ export async function getOrganizationUsers( client: typeof graphql, org: string, tokenType: GithubCredentialType, - userNamespace?: string, + userTransformer: UserTransformer = defaultUserTransformer, ): Promise<{ users: UserEntity[] }> { const query = ` query users($org: String!, $email: Boolean!, $cursor: String) { @@ -111,7 +136,8 @@ export async function getOrganizationUsers( bio, email @include(if: $email), login, - name + name, + organizationVerifiedDomainEmails(login: $org) } } } @@ -119,44 +145,24 @@ export async function getOrganizationUsers( // There is no user -> teams edge, so we leave the memberships empty for // now and let the team iteration handle it instead - const mapper = (user: User) => { - const entity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'User', - metadata: { - name: user.login, - annotations: { - 'github.com/user-login': user.login, - }, - }, - spec: { - profile: {}, - memberOf: [], - }, - }; - - if (userNamespace) entity.metadata.namespace = userNamespace; - if (user.bio) entity.metadata.description = user.bio; - if (user.name) entity.spec.profile!.displayName = user.name; - if (user.email) entity.spec.profile!.email = user.email; - if (user.avatarUrl) entity.spec.profile!.picture = user.avatarUrl; - - return entity; - }; const users = await queryWithPaging( client, query, + org, r => r.organization?.membersWithRole, - mapper, - { org, email: tokenType === 'token' }, + userTransformer, + { + org, + email: tokenType === 'token', + }, ); return { users }; } /** - * Gets all the teams out of a GitHub organization. + * Gets all the teams out of a Github organization. * * Note that the teams will not have any relations apart from parent filled in. * @@ -166,10 +172,9 @@ export async function getOrganizationUsers( export async function getOrganizationTeams( client: typeof graphql, org: string, - orgNamespace?: string, + teamTransformer: TeamTransformer = defaultOrganizationTeamTransformer, ): Promise<{ groups: GroupEntity[]; - groupMemberUsers: Map; }> { const query = ` query teams($org: String!, $cursor: String) { @@ -193,86 +198,51 @@ export async function getOrganizationTeams( } }`; - // Gets populated inside the mapper below - const groupMemberUsers = new Map(); + const materialisedTeams = async ( + item: GithubTeamResponse, + ctx: TransformerContext, + ): Promise => { + const memberNames: GithubUser[] = []; - const mapper = async (team: Team) => { - const annotations: { [annotationName: string]: string } = { - 'github.com/team-slug': team.combinedSlug, - }; - - if (team.editTeamUrl) { - annotations['backstage.io/edit-url'] = team.editTeamUrl; - } - - const entity: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Group', - metadata: { - name: team.slug, - annotations, - }, - spec: { - type: 'team', - profile: {}, - children: [], - }, - }; - - if (orgNamespace) { - entity.metadata.namespace = orgNamespace; - } - - if (team.description) { - entity.metadata.description = team.description; - } - if (team.name) { - entity.spec.profile!.displayName = team.name; - } - if (team.avatarUrl) { - entity.spec.profile!.picture = team.avatarUrl; - } - if (team.parentTeam) { - entity.spec.parent = team.parentTeam.slug; - } - - const memberNames: string[] = []; - const groupKey = orgNamespace ? `${orgNamespace}/${team.slug}` : team.slug; - groupMemberUsers.set(groupKey, memberNames); - - if (!team.members.pageInfo.hasNextPage) { + if (!item.members.pageInfo.hasNextPage) { // We got all the members in one go, run the fast path - for (const user of team.members.nodes) { - memberNames.push(user.login); + for (const user of item.members.nodes) { + memberNames.push(user); } } else { // There were more than a hundred immediate members - run the slow // path of fetching them explicitly - const { members } = await getTeamMembers(client, org, team.slug); + const { members } = await getTeamMembers(ctx.client, ctx.org, item.slug); for (const userLogin of members) { memberNames.push(userLogin); } } - return entity; + const team: GithubTeam = { + ...item, + members: memberNames, + }; + + return await teamTransformer(team, ctx); }; const groups = await queryWithPaging( client, query, + org, r => r.organization?.teams, - mapper, + materialisedTeams, { org }, ); - return { groups, groupMemberUsers }; + return { groups }; } export async function getOrganizationRepositories( client: typeof graphql, org: string, catalogPath: string, -): Promise<{ repositories: Repository[] }> { +): Promise<{ repositories: RepositoryResponse[] }> { let relativeCatalogPathRef: string; // We must strip the leading slash or the query for objects does not work if (catalogPath.startsWith('/')) { @@ -321,8 +291,9 @@ export async function getOrganizationRepositories( const repositories = await queryWithPaging( client, query, + org, r => r.repositoryOwner?.repositories, - x => x, + async x => x, { org, catalogPathRef }, ); @@ -330,7 +301,7 @@ export async function getOrganizationRepositories( } /** - * Gets all the users out of a GitHub organization. + * Gets all the users out of a Github organization. * * Note that the users will not have their memberships filled in. * @@ -342,7 +313,7 @@ export async function getTeamMembers( client: typeof graphql, org: string, teamSlug: string, -): Promise<{ members: string[] }> { +): Promise<{ members: GithubUser[] }> { const query = ` query members($org: String!, $teamSlug: String!, $cursor: String) { organization(login: $org) { @@ -358,8 +329,9 @@ export async function getTeamMembers( const members = await queryWithPaging( client, query, + org, r => r.organization?.team?.members, - user => user.login, + async user => user, { org, teamSlug }, ); @@ -379,7 +351,7 @@ export async function getTeamMembers( * @param query - The query to execute * @param connection - A function that, given the response, picks out the actual * Connection object that's being iterated - * @param mapper - A function that, given one of the nodes in the Connection, + * @param transformer - A function that, given one of the nodes in the Connection, * returns the model mapped form of it * @param variables - The variable values that the query needs, minus the cursor */ @@ -391,8 +363,12 @@ export async function queryWithPaging< >( client: typeof graphql, query: string, + org: string, connection: (response: Response) => Connection | undefined, - mapper: (item: GraphqlType) => Promise | OutputType, + transformer: ( + item: GraphqlType, + ctx: TransformerContext, + ) => Promise, variables: Variables, ): Promise { const result: OutputType[] = []; @@ -410,7 +386,15 @@ export async function queryWithPaging< } for (const node of conn.nodes) { - result.push(await mapper(node)); + const transformedNode = await transformer(node, { + client, + query, + org, + }); + + if (transformedNode) { + result.push(transformedNode); + } } if (!conn.pageInfo.hasNextPage) { diff --git a/plugins/catalog-backend-module-github/src/lib/index.ts b/plugins/catalog-backend-module-github/src/lib/index.ts index 6402b69121..14f78fdeee 100644 --- a/plugins/catalog-backend-module-github/src/lib/index.ts +++ b/plugins/catalog-backend-module-github/src/lib/index.ts @@ -20,6 +20,15 @@ export { getOrganizationRepositories, getOrganizationTeams, getOrganizationUsers, + type GithubUser, + type GithubTeam, } from './github'; +export { + type UserTransformer, + defaultUserTransformer, + type TeamTransformer, + defaultOrganizationTeamTransformer, + type TransformerContext, +} from './defaultTransformers'; export { assignGroupsToUsers, buildOrgHierarchy } from './org'; export { parseGithubOrgUrl } from './util'; diff --git a/plugins/catalog-backend-module-github/src/lib/org.test.ts b/plugins/catalog-backend-module-github/src/lib/org.test.ts index 8a8f47642a..1cdc55a4da 100644 --- a/plugins/catalog-backend-module-github/src/lib/org.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/org.test.ts @@ -68,13 +68,47 @@ describe('buildOrgHierarchy', () => { describe('assignGroupsToUsers', () => { it('should assign groups to users', () => { const users: UserEntity[] = [u('u1'), u('u2')]; - const groupMemberUsers = new Map([ - ['g1', ['u1', 'u2']], - ['g2', ['u2']], - ['g3', ['u3']], - ]); - assignGroupsToUsers(users, groupMemberUsers); + const groups: GroupEntity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'g1', + }, + spec: { + type: 'team', + children: [], + members: ['default/u1', 'default/u2'], + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'g2', + }, + spec: { + type: 'team', + children: [], + members: ['u2'], + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'g3', + }, + spec: { + type: 'team', + children: [], + members: ['u3'], + }, + }, + ]; + + assignGroupsToUsers(users, groups); expect(users[0].spec.memberOf).toEqual(['g1']); expect(users[1].spec.memberOf).toEqual(['g1', 'g2']); diff --git a/plugins/catalog-backend-module-github/src/lib/org.ts b/plugins/catalog-backend-module-github/src/lib/org.ts index 79a96b5e71..4336f2d49c 100644 --- a/plugins/catalog-backend-module-github/src/lib/org.ts +++ b/plugins/catalog-backend-module-github/src/lib/org.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { + DEFAULT_NAMESPACE, + GroupEntity, + parseEntityRef, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; export function buildOrgHierarchy(groups: GroupEntity[]) { const groupsByName = new Map(groups.map(g => [g.metadata.name, g])); @@ -52,12 +58,29 @@ export function buildOrgHierarchy(groups: GroupEntity[]) { // Ensure that users have their direct group memberships. export function assignGroupsToUsers( users: UserEntity[], - groupMemberUsers: Map, + groups: GroupEntity[], ) { - const usersByName = new Map(users.map(u => [u.metadata.name, u])); - for (const [groupName, userNames] of groupMemberUsers.entries()) { - for (const userName of userNames) { - const user = usersByName.get(userName); + const groupMemberUsers = new Map( + groups.map(group => { + const groupKey = + group.metadata.namespace && + group.metadata.namespace !== DEFAULT_NAMESPACE + ? `${group.metadata.namespace}/${group.metadata.name}` + : group.metadata.name; + // Fully qualify member refs so they can be keyed off of since they may contain namespace prefixes + return [ + groupKey, + group.spec.members?.map(m => + stringifyEntityRef(parseEntityRef(m, { defaultKind: 'user' })), + ) || [], + ]; + }), + ); + + const usersByRef = new Map(users.map(u => [stringifyEntityRef(u), u])); + for (const [groupName, userRefs] of groupMemberUsers.entries()) { + for (const ref of userRefs) { + const user = usersByRef.get(ref); if (user && !user.spec.memberOf?.includes(groupName)) { if (!user.spec.memberOf) { user.spec.memberOf = []; diff --git a/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts index 7bb73990b3..7777da6b1b 100644 --- a/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-github/src/processors/GithubMultiOrgReaderProcessor.ts @@ -14,6 +14,12 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + GroupEntity, + stringifyEntityRef, + UserEntity, +} from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { DefaultGithubCredentialsProvider, @@ -32,11 +38,16 @@ import { import { graphql } from '@octokit/graphql'; import { Logger } from 'winston'; import { + assignGroupsToUsers, buildOrgHierarchy, + defaultOrganizationTeamTransformer, + defaultUserTransformer, getOrganizationTeams, getOrganizationUsers, GithubMultiOrgConfig, readGithubMultiOrgConfig, + TeamTransformer, + UserTransformer, } from '../lib'; /** @@ -57,6 +68,8 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { options: { logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; + userTransformer?: UserTransformer; + teamTransformer?: TeamTransformer; }, ) { const c = config.getOptionalConfig('catalog.processors.githubMultiOrg'); @@ -69,12 +82,16 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { }); } - constructor(options: { - integrations: ScmIntegrationRegistry; - logger: Logger; - orgs: GithubMultiOrgConfig; - githubCredentialsProvider?: GithubCredentialsProvider; - }) { + constructor( + private options: { + integrations: ScmIntegrationRegistry; + logger: Logger; + orgs: GithubMultiOrgConfig; + githubCredentialsProvider?: GithubCredentialsProvider; + userTransformer?: UserTransformer; + teamTransformer?: TeamTransformer; + }, + ) { this.integrations = options.integrations; this.logger = options.logger; this.orgs = options.orgs; @@ -130,12 +147,40 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { client, orgConfig.name, tokenType, - orgConfig.userNamespace, + async (githubUser, ctx): Promise => { + const result = this.options.userTransformer + ? await this.options.userTransformer(githubUser, ctx) + : await defaultUserTransformer(githubUser, ctx); + + if (result) { + result.metadata.namespace = orgConfig.userNamespace; + } + + return result; + }, ); - const { groups, groupMemberUsers } = await getOrganizationTeams( + + const { groups } = await getOrganizationTeams( client, orgConfig.name, - orgConfig.groupNamespace, + async (team, ctx): Promise => { + const result = this.options.teamTransformer + ? await this.options.teamTransformer(team, ctx) + : await defaultOrganizationTeamTransformer(team, ctx); + + if (result) { + result.metadata.namespace = orgConfig.groupNamespace; + // Group `spec.members` inherits the namespace of it's group so need to explicitly specify refs here + result.spec.members = team.members.map( + user => + `${orgConfig.userNamespace ?? DEFAULT_NAMESPACE}/${ + user.login + }`, + ); + } + + return result; + }, ); const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); @@ -143,23 +188,18 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { `Read ${users.length} GitHub users and ${groups.length} GitHub teams from ${orgConfig.name} in ${duration} seconds`, ); - let prefix: string = orgConfig.userNamespace ?? ''; - if (prefix.length > 0) prefix += '/'; - - users.forEach(u => { - if (!allUsersMap.has(prefix + u.metadata.name)) { - allUsersMap.set(prefix + u.metadata.name, u); + // Grab current users from `allUsersMap` if they already exist in our + // pending users so we can append to their group membership relations + const pendingUsers = users.map(u => { + const userRef = stringifyEntityRef(u); + if (!allUsersMap.has(userRef)) { + allUsersMap.set(userRef, u); } + + return allUsersMap.get(userRef); }); - for (const [groupName, userNames] of groupMemberUsers.entries()) { - for (const userName of userNames) { - const user = allUsersMap.get(prefix + userName); - if (user && !user.spec.memberOf.includes(groupName)) { - user.spec.memberOf.push(groupName); - } - } - } + assignGroupsToUsers(pendingUsers, groups); buildOrgHierarchy(groups); for (const group of groups) { @@ -172,6 +212,8 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { } } + // Emit all users at the end after all orgs have been processed + // so all memberships across org groups are accounted for const allUsers = Array.from(allUsersMap.values()); for (const user of allUsers) { emit(processingResult.entity(location, user)); diff --git a/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.test.ts b/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.test.ts index 2bb0c538eb..ce81e2cf42 100644 --- a/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.test.ts +++ b/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.test.ts @@ -85,7 +85,10 @@ describe('GithubOrgReaderProcessor', () => { mockClient .mockResolvedValueOnce({ organization: { - membersWithRole: { pageInfo: { hasNextPage: false }, nodes: [{}] }, + membersWithRole: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'foo' }], + }, }, }) .mockResolvedValueOnce({ @@ -93,7 +96,12 @@ describe('GithubOrgReaderProcessor', () => { teams: { pageInfo: { hasNextPage: false }, nodes: [ - { members: { pageInfo: { hasNextPage: false }, nodes: [{}] } }, + { + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'foo' }], + }, + }, ], }, }, @@ -134,7 +142,10 @@ describe('GithubOrgReaderProcessor', () => { mockClient .mockResolvedValueOnce({ organization: { - membersWithRole: { pageInfo: { hasNextPage: false }, nodes: [{}] }, + membersWithRole: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'foo' }], + }, }, }) .mockResolvedValueOnce({ @@ -142,7 +153,12 @@ describe('GithubOrgReaderProcessor', () => { teams: { pageInfo: { hasNextPage: false }, nodes: [ - { members: { pageInfo: { hasNextPage: false }, nodes: [{}] } }, + { + members: { + pageInfo: { hasNextPage: false }, + nodes: [{ login: 'foo' }], + }, + }, ], }, }, diff --git a/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.ts index 0ef81ac907..90c168ccf4 100644 --- a/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-github/src/processors/GithubOrgReaderProcessor.ts @@ -101,17 +101,14 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { this.logger.info('Reading GitHub users and groups'); const { users } = await getOrganizationUsers(client, org, tokenType); - const { groups, groupMemberUsers } = await getOrganizationTeams( - client, - org, - ); + const { groups } = await getOrganizationTeams(client, org); const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); this.logger.debug( `Read ${users.length} GitHub users and ${groups.length} GitHub groups in ${duration} seconds`, ); - assignGroupsToUsers(users, groupMemberUsers); + assignGroupsToUsers(users, groups); buildOrgHierarchy(groups); // Done! 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 cc9a9bfee3..a41404ad9a 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts @@ -24,6 +24,7 @@ import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GithubEntityProvider } from './GithubEntityProvider'; import * as helpers from '../lib/github'; +import { EventParams } from '@backstage/plugin-events-node'; jest.mock('../lib/github', () => { return { @@ -716,4 +717,392 @@ describe('GithubEntityProvider', () => { expect(providers).toHaveLength(1); expect(providers[0].getProviderName()).toEqual('github-provider:default'); }); + + it('apply delta update on added files from push event', async () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + catalog: { + providers: { + github: { + organization: 'test-org', + }, + }, + }, + }); + + const provider = GithubEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event: EventParams = { + topic: 'github.push', + metadata: { + 'x-github-event': 'push', + }, + eventPayload: { + ref: 'refs/heads/main', + repository: { + name: 'teste-1', + url: 'https://github.com/test-org/test-repo', + default_branch: 'main', + stargazers: 0, + master_branch: 'main', + organization: 'test-org', + topics: [], + }, + created: true, + deleted: false, + forced: false, + commits: [ + { + added: ['new-file.yaml'], + removed: [], + modified: [], + }, + { + added: ['catalog-info.yaml'], + removed: [], + modified: [], + }, + ], + }, + }; + const url = + 'https://github.com/test-org/test-repo/blob/main/catalog-info.yaml'; + const expectedEntities = [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:${url}`, + 'backstage.io/managed-by-origin-location': `url:${url}`, + }, + name: 'generated-8688630f57e421bc85f12b9828ed7dad6aff3bb3', + }, + spec: { + presence: 'optional', + target: `${url}`, + type: 'url', + }, + }, + locationKey: 'github-provider:default', + }, + ]; + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: expectedEntities, + removed: [], + }); + }); + + it('apply delta update on removed files from push event', async () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + catalog: { + providers: { + github: { + organization: 'test-org', + }, + }, + }, + }); + + const provider = GithubEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event: EventParams = { + topic: 'github.push', + metadata: { + 'x-github-event': 'push', + }, + eventPayload: { + ref: 'refs/heads/main', + repository: { + name: 'teste-1', + url: 'https://github.com/test-org/test-repo', + default_branch: 'main', + stargazers: 0, + master_branch: 'main', + organization: 'test-org', + topics: [], + }, + created: true, + deleted: false, + forced: false, + commits: [ + { + added: ['new-file.yaml'], + removed: [], + modified: [], + }, + { + added: [], + removed: ['catalog-info.yaml'], + modified: [], + }, + ], + }, + }; + const url = + 'https://github.com/test-org/test-repo/blob/main/catalog-info.yaml'; + const expectedEntities = [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:${url}`, + 'backstage.io/managed-by-origin-location': `url:${url}`, + }, + name: 'generated-8688630f57e421bc85f12b9828ed7dad6aff3bb3', + }, + spec: { + presence: 'optional', + target: `${url}`, + type: 'url', + }, + }, + locationKey: 'github-provider:default', + }, + ]; + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [], + removed: expectedEntities, + }); + }); + + it('apply refresh call on modified files from push event', async () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + catalog: { + providers: { + github: { + organization: 'test-org', + }, + }, + }, + }); + + const provider = GithubEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event: EventParams = { + topic: 'github.push', + metadata: { + 'x-github-event': 'push', + }, + eventPayload: { + ref: 'refs/heads/main', + repository: { + name: 'teste-1', + url: 'https://github.com/test-org/test-repo', + default_branch: 'main', + stargazers: 0, + master_branch: 'main', + organization: 'test-org', + topics: [], + }, + created: true, + deleted: false, + forced: false, + commits: [ + { + added: ['new-file.yaml'], + removed: [], + modified: [], + }, + { + added: [], + removed: [], + modified: ['catalog-info.yaml'], + }, + ], + }, + }; + + await provider.onEvent(event); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ + keys: [ + 'url:https://github.com/test-org/test-repo/tree/main/catalog-info.yaml', + 'url:https://github.com/test-org/test-repo/blob/main/catalog-info.yaml', + ], + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); + + it('should process repository when match filters from push event', async () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + catalog: { + providers: { + github: { + organization: 'test-org', + filters: { + branch: 'my-special-branch', + repository: 'test-repo', + }, + }, + }, + }, + }); + + const provider = GithubEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event: EventParams = { + topic: 'github.push', + metadata: { + 'x-github-event': 'push', + }, + eventPayload: { + ref: 'refs/heads/my-special-branch', + repository: { + name: 'test-repo', + url: 'https://github.com/test-org/test-repo', + default_branch: 'main', + stargazers: 0, + master_branch: 'main', + organization: 'test-org', + topics: [], + }, + created: true, + deleted: false, + forced: false, + commits: [ + { + added: ['new-file.yaml'], + removed: [], + modified: [], + }, + { + added: [], + removed: [], + modified: ['catalog-info.yaml'], + }, + ], + }, + }; + + await provider.onEvent(event); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ + keys: [ + 'url:https://github.com/test-org/test-repo/tree/my-special-branch/catalog-info.yaml', + 'url:https://github.com/test-org/test-repo/blob/my-special-branch/catalog-info.yaml', + ], + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); + + it("should skip process when didn't match filters from push event", async () => { + const schedule = new PersistingTaskRunner(); + const config = new ConfigReader({ + catalog: { + providers: { + github: { + organization: 'test-org', + filters: { + repository: 'only-special-repository', + }, + }, + }, + }, + }); + + const provider = GithubEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event: EventParams = { + topic: 'github.push', + metadata: { + 'x-github-event': 'push', + }, + eventPayload: { + ref: 'refs/heads/main', + repository: { + name: 'teste-1', + url: 'https://github.com/test-org/test-repo', + default_branch: 'main', + stargazers: 0, + master_branch: 'main', + organization: 'test-org', + topics: [], + }, + created: true, + deleted: false, + forced: false, + commits: [ + { + added: ['new-file.yaml'], + removed: [], + modified: [], + }, + { + added: [], + removed: [], + modified: ['catalog-info.yaml'], + }, + ], + }, + }; + + await provider.onEvent(event); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); }); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts index 6d3badf865..8816d8ee80 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts @@ -24,12 +24,14 @@ import { SingleInstanceGithubCredentialsProvider, } from '@backstage/integration'; import { + DeferredEntity, EntityProvider, EntityProviderConnection, - LocationSpec, locationSpecToLocationEntity, } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; + import { graphql } from '@octokit/graphql'; import * as uuid from 'uuid'; import { Logger } from 'winston'; @@ -37,9 +39,23 @@ import { readProviderConfigs, GithubEntityProviderConfig, } from './GithubEntityProviderConfig'; -import { getOrganizationRepositories, Repository } from '../lib/github'; +import { getOrganizationRepositories } from '../lib/github'; import { satisfiesTopicFilter } from '../lib/util'; +import { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; +import { PushEvent, Commit } from '@octokit/webhooks-types'; + +const TOPIC_REPO_PUSH = 'github.push'; + +type Repository = { + name: string; + url: string; + isArchived: boolean; + repositoryTopics: string[]; + defaultBranchRef?: string; + isCatalogInfoFilePresent: boolean; +}; + /** * Discovers catalog files located in [GitHub](https://github.com). * The provider will search your GitHub account and register catalog files matching the configured path @@ -48,7 +64,7 @@ import { satisfiesTopicFilter } from '../lib/util'; * * @public */ -export class GithubEntityProvider implements EntityProvider { +export class GithubEntityProvider implements EntityProvider, EventSubscriber { private readonly config: GithubEntityProviderConfig; private readonly logger: Logger; private readonly integration: GithubIntegrationConfig; @@ -190,19 +206,25 @@ export class GithubEntityProvider implements EntityProvider { headers, }); - const { repositories } = await getOrganizationRepositories( - client, - organization, - catalogPath, - ); + const { repositories: repositoriesFromGithub } = + await getOrganizationRepositories(client, organization, catalogPath); + const repositories = repositoriesFromGithub.map(r => { + return { + url: r.url, + name: r.name, + defaultBranchRef: r.defaultBranchRef?.name, + repositoryTopics: r.repositoryTopics.nodes.map(t => t.topic.name), + isArchived: r.isArchived, + isCatalogInfoFilePresent: + r.catalogInfoFile?.__typename === 'Blob' && + r.catalogInfoFile.text !== '', + }; + }); if (this.config.validateLocationsExist) { - return repositories.filter(repository => { - return ( - repository.catalogInfoFile?.__typename === 'Blob' && - repository.catalogInfoFile.text !== '' - ); - }); + return repositories.filter( + repository => repository.isCatalogInfoFilePresent, + ); } return repositories; @@ -213,14 +235,12 @@ export class GithubEntityProvider implements EntityProvider { const topicFilters = this.config.filters?.topic; const matchingRepositories = repositories.filter(r => { - const repoTopics: string[] = r.repositoryTopics.nodes.map( - node => node.topic.name, - ); + const repoTopics: string[] = r.repositoryTopics; return ( !r.isArchived && (!repositoryFilter || repositoryFilter.test(r.name)) && satisfiesTopicFilter(repoTopics, topicFilters) && - r.defaultBranchRef?.name + r.defaultBranchRef ); }); return matchingRepositories; @@ -228,7 +248,7 @@ export class GithubEntityProvider implements EntityProvider { private createLocationUrl(repository: Repository): string { const branch = - this.config.filters?.branch || repository.defaultBranchRef?.name || '-'; + this.config.filters?.branch || repository.defaultBranchRef || '-'; const catalogFile = this.config.catalogPath.startsWith('/') ? this.config.catalogPath.substring(1) : this.config.catalogPath; @@ -242,6 +262,151 @@ export class GithubEntityProvider implements EntityProvider { presence: 'optional', }; } + + /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.onEvent} */ + async onEvent(params: EventParams): Promise { + this.logger.debug(`Received event from ${params.topic}`); + if (params.topic !== TOPIC_REPO_PUSH) { + return; + } + + await this.onRepoPush(params.eventPayload as PushEvent); + } + + /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.supportsEventTopics} */ + supportsEventTopics(): string[] { + return [TOPIC_REPO_PUSH]; + } + + private async onRepoPush(event: PushEvent) { + if (!this.connection) { + throw new Error('Not initialized'); + } + + const repoName = event.repository.name; + const repoUrl = event.repository.url; + this.logger.debug(`handle github:push event for ${repoName} - ${repoUrl}`); + + const branch = + this.config.filters?.branch || event.repository.default_branch; + + if (!event.ref.includes(branch)) { + this.logger.debug(`skipping push event from ref ${event.ref}`); + return; + } + + const repository: Repository = { + url: event.repository.url, + name: event.repository.name, + defaultBranchRef: event.repository.default_branch, + repositoryTopics: event.repository.topics, + isArchived: event.repository.archived, + // we can consider this file present because + // only the catalog file will be recovered from the commits + isCatalogInfoFilePresent: true, + }; + + const matchingTargets = this.matchesFilters([repository]); + if (matchingTargets.length === 0) { + this.logger.debug( + `skipping push event from repository ${repoName} because didn't match provider filters`, + ); + return; + } + + // the commit has information about the files (added,removed,modified) + // 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, + branch, + event.commits, + (commit: Commit) => [...commit.added], + ); + const removed = this.collectDeferredEntitiesFromCommit( + event.repository.url, + branch, + event.commits, + (commit: Commit) => [...commit.removed], + ); + const modified = this.collectFilesFromCommit( + event.commits, + (commit: Commit) => [...commit.modified], + ); + + if (modified.length > 0) { + await this.connection.refresh({ + keys: [ + ...modified.map( + filePath => + `url:${event.repository.url}/tree/${branch}/${filePath}`, + ), + ...modified.map( + filePath => + `url:${event.repository.url}/blob/${branch}/${filePath}`, + ), + ], + }); + } + + if (added.length > 0 || removed.length > 0) { + await this.connection.applyMutation({ + type: 'delta', + added: added, + removed: removed, + }); + } + + this.logger.info( + `Processed Github push event: added ${added.length} - removed ${removed.length} - modified ${modified.length}`, + ); + } + + private collectDeferredEntitiesFromCommit( + repositoryUrl: string, + branch: string, + commits: Commit[], + transformOperation: (commit: Commit) => string[], + ): DeferredEntity[] { + const catalogFiles = this.collectFilesFromCommit( + commits, + transformOperation, + ); + return this.toDeferredEntities( + catalogFiles.map( + filePath => `${repositoryUrl}/blob/${branch}/${filePath}`, + ), + ); + } + + private collectFilesFromCommit( + commits: Commit[], + transformOperation: (commit: Commit) => string[], + ): string[] { + const catalogFile = this.config.catalogPath.startsWith('/') + ? this.config.catalogPath.substring(1) + : this.config.catalogPath; + + return commits + .map(transformOperation) + .flat() + .filter(file => catalogFile.includes(file)); + } + + private toDeferredEntities(targets: string[]): DeferredEntity[] { + return targets + .map(target => { + const location = GithubEntityProvider.toLocationSpec(target); + + return locationSpecToLocationEntity({ location }); + }) + .map(entity => { + return { + locationKey: this.getProviderName(), + entity: entity, + }; + }); + } } /* diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts index b3fc5022ec..0b8765b93e 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts @@ -163,6 +163,7 @@ describe('GithubOrgEntityProvider', () => { picture: 'http://example.com/team.jpeg', }, type: 'team', + members: ['a'], }, }, locationKey: 'github-org-provider:my-id', diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts index 2fc8ce7300..210df46363 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts @@ -43,6 +43,7 @@ import { getOrganizationUsers, parseGithubOrgUrl, } from '../lib'; +import { TeamTransformer, UserTransformer } from '../lib/defaultTransformers'; /** * Options for {@link GithubOrgEntityProvider}. @@ -88,6 +89,16 @@ export interface GithubOrgEntityProviderOptions { * Optionally supply a custom credentials provider, replacing the default one. */ githubCredentialsProvider?: GithubCredentialsProvider; + + /** + * Optionally include a user transformer for transforming from GitHub users to User Entities + */ + userTransformer?: UserTransformer; + + /** + * Optionally include a team transformer for transforming from GitHub teams to Group Entities + */ + teamTransformer?: TeamTransformer; } // TODO: Consider supporting an (optional) webhook that reacts on org changes @@ -123,6 +134,8 @@ export class GithubOrgEntityProvider implements EntityProvider { githubCredentialsProvider: options.githubCredentialsProvider || DefaultGithubCredentialsProvider.fromIntegrations(integrations), + userTransformer: options.userTransformer, + teamTransformer: options.teamTransformer, }); provider.schedule(options.schedule); @@ -137,6 +150,8 @@ export class GithubOrgEntityProvider implements EntityProvider { gitHubConfig: GithubIntegrationConfig; logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; + userTransformer?: UserTransformer; + teamTransformer?: TeamTransformer; }, ) { this.credentialsProvider = @@ -177,12 +192,19 @@ export class GithubOrgEntityProvider implements EntityProvider { }); const { org } = parseGithubOrgUrl(this.options.orgUrl); - const { users } = await getOrganizationUsers(client, org, tokenType); - const { groups, groupMemberUsers } = await getOrganizationTeams( + const { users } = await getOrganizationUsers( client, org, + tokenType, + this.options.userTransformer, ); - assignGroupsToUsers(users, groupMemberUsers); + const { groups } = await getOrganizationTeams( + client, + org, + this.options.teamTransformer, + ); + + assignGroupsToUsers(users, groups); buildOrgHierarchy(groups); const { markCommitComplete } = markReadComplete({ users, groups }); diff --git a/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.test.ts index b555927d2c..d8ebc8d0ad 100644 --- a/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.test.ts @@ -15,11 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { - configServiceRef, - loggerServiceRef, - schedulerServiceRef, -} from '@backstage/backend-plugin-api'; +import { coreServices } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskScheduleDefinition, @@ -66,9 +62,9 @@ describe('githubEntityProviderCatalogModule', () => { await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], services: [ - [configServiceRef, config], - [loggerServiceRef, getVoidLogger()], - [schedulerServiceRef, scheduler], + [coreServices.config, config], + [coreServices.logger, getVoidLogger()], + [coreServices.scheduler, scheduler], ], features: [githubEntityProviderCatalogModule()], }); diff --git a/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.ts index 8fdee886a3..e61543ea13 100644 --- a/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-github/src/service/GithubEntityProviderCatalogModule.ts @@ -17,9 +17,7 @@ import { createBackendModule, loggerToWinstonLogger, - configServiceRef, - loggerServiceRef, - schedulerServiceRef, + coreServices, } from '@backstage/backend-plugin-api'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; import { GithubEntityProvider } from '../providers/GithubEntityProvider'; @@ -36,9 +34,9 @@ export const githubEntityProviderCatalogModule = createBackendModule({ env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, - config: configServiceRef, - logger: loggerServiceRef, - scheduler: schedulerServiceRef, + config: coreServices.config, + logger: coreServices.logger, + scheduler: coreServices.scheduler, }, async init({ catalog, config, logger, scheduler }) { catalog.addEntityProvider( diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 18cce9e01e..6fe5cca4bf 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.1.10-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.1.9 + +### Patch Changes + +- 6bb046bcbe: Add `gitlabDiscoveryEntityProviderCatalogModule` (new backend-plugin-api, alpha). +- 81cedb5033: `GitlabDiscoveryEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + https://backstage.io/docs/integrations/gitlab/discovery + +- 4c9f7847e4: Updated dependency `msw` to `^0.48.0` while moving it to be a dev dependency. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + ## 0.1.9-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index e556d4b2de..227528f416 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", "description": "A Backstage catalog backend module that helps integrate towards GitLab", - "version": "0.1.9-next.0", + "version": "0.1.10-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -44,7 +44,6 @@ "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", "lodash": "^4.17.21", - "msw": "^0.47.0", "node-fetch": "^2.6.7", "uuid": "^8.0.0", "winston": "^3.2.1" @@ -54,7 +53,8 @@ "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", "@types/uuid": "^8.0.0", - "luxon": "^3.0.0" + "luxon": "^3.0.0", + "msw": "^0.49.0" }, "files": [ "alpha", diff --git a/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.test.ts index 0b803734b1..ca8d86f555 100644 --- a/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.test.ts @@ -15,11 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { - configServiceRef, - loggerServiceRef, - schedulerServiceRef, -} from '@backstage/backend-plugin-api'; +import { coreServices } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskScheduleDefinition, @@ -78,9 +74,9 @@ describe('gitlabDiscoveryEntityProviderCatalogModule', () => { await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], services: [ - [configServiceRef, config], - [loggerServiceRef, getVoidLogger()], - [schedulerServiceRef, scheduler], + [coreServices.config, config], + [coreServices.logger, getVoidLogger()], + [coreServices.scheduler, scheduler], ], features: [gitlabDiscoveryEntityProviderCatalogModule()], }); diff --git a/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.ts index dc11841e20..75a8de263f 100644 --- a/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-gitlab/src/service/GitlabDiscoveryEntityProviderCatalogModule.ts @@ -17,9 +17,7 @@ import { createBackendModule, loggerToWinstonLogger, - configServiceRef, - loggerServiceRef, - schedulerServiceRef, + coreServices, } from '@backstage/backend-plugin-api'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; import { GitlabDiscoveryEntityProvider } from '../providers'; @@ -35,10 +33,10 @@ export const gitlabDiscoveryEntityProviderCatalogModule = createBackendModule({ register(env) { env.registerInit({ deps: { - config: configServiceRef, + config: coreServices.config, catalog: catalogProcessingExtensionPoint, - logger: loggerServiceRef, - scheduler: schedulerServiceRef, + logger: coreServices.logger, + scheduler: coreServices.scheduler, }, async init({ config, catalog, logger, scheduler }) { catalog.addEntityProvider( diff --git a/plugins/catalog-backend-module-incremental-ingestion/.eslintrc.js b/plugins/catalog-backend-module-incremental-ingestion/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md new file mode 100644 index 0000000000..3780f8a826 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -0,0 +1,31 @@ +# @backstage/plugin-catalog-backend-module-incremental-ingestion + +## 0.1.0-next.0 + +### Minor Changes + +- 98c643a1a2: Introduces incremental entity providers, which are used for streaming very large data sources into the catalog. + +### Patch Changes + +- de8a975911: Changed to use native `AbortController` and `AbortSignal` from Node.js, instead + of the one from `node-abort-controller`. This is possible now that the minimum + supported Node.js version of the project is 16. + + Note that their interfaces are very slightly different, but typically not in a + way that matters to consumers. If you see any typescript errors as a direct + result from this, they are compatible with each other in the ways that we + interact with them, and should be possible to type-cast across without ill + effects. + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/backend-test-utils@0.1.31-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 diff --git a/plugins/catalog-backend-module-incremental-ingestion/README.md b/plugins/catalog-backend-module-incremental-ingestion/README.md new file mode 100644 index 0000000000..0a646e4857 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/README.md @@ -0,0 +1,340 @@ +# `@backstage/plugin-catalog-backend-module-incremental-ingestion` + +The Incremental Ingestion catalog backend module provides an Incremental Entity Provider that can be used to ingest data from sources using delta mutations, while retaining the orphan prevention mechanism provided by full mutations. + +## Why did we create it? + +Backstage provides an [Entity Provider mechanism that has two kinds of mutations](https://backstage.io/docs/features/software-catalog/external-integrations#provider-mutations): `delta` and `full`. `delta` mutations tell Backstage Catalog which entities should be added and removed from the catalog. `full` mutation accepts a list of entities and automatically computes which entities must be removed by comparing the provided entities against existing entities to create a diff between the two sets. These two kinds of mutations are convenient for different kinds of data sources. A `delta` mutation can be used with a data source that emits UPDATE and DELETE events for its data. A `full` mutation is useful for APIs that produce fewer entities than can fit in Backstage processes' memory. + +Unfortunately, these two kinds of mutations are insufficient for very large data sources for the following reasons, + +1. Even when the API provides DELETE events, we still need a way to create the initial list of entities. For example, if you ingest all repositories from GitHub into Backstage and you use webhooks, you still need the initial list of entities. +2. A `delta` mutation can not guarantee that mutations will not be missed. For example, if your Backstage portal is down while receiving a DELETE event, you might miss the event which leaves your catalog in an unclear state. How can you replay the missed events? Some data sources, like GitHub, provide an API for replaying missed events, but this increases complexity and is not available on all APIs. +3. Addressing the above two use case with `full` mutation is not an option on very large datasets because a `full` mutation requires that all entities are in memory to create a diff. If your data source has 100k+ records, this can easily cause your processes to run out of memory. +4. In cases when you can use `full` mutation, committing many entities into the processing pipeline fills up the processing queue and delays the processing of entities from other entity providers. + +We created the Incremental Entity Provider to address all of the above issues. The Incremental Entity Provider addresses these issues with a combination of `delta` mutations and a mark-and-sweep mechanism. Instead of doing a single `full` mutation, it performs a series of bursts. At the end of each burst, the Incremental Entity Provider performs the following operations, + +1. Marks each received entity in the database. +2. Commits all of the entities with a `delta` mutation. + +Incremental Entity Providers will wait a configurable interval before proceeding to the next burst. + +Once the source has no more results, Incremental Entity Provider compares all entities annotated with `@backstage/incremental-entity-provider: ` against all marked entities to determine which entities committed by same entity provider were not marked during the last ingestion cycle. All unmarked entities are deleted at the end of the cycle. The Incremental Entity Provider rests for a fixed internal before restarting the ingestion process. + +![Diagram of execution of an Incremental Entity Provider](https://user-images.githubusercontent.com/74687/185822734-ee6279c7-64fa-46b9-9aa8-d4092ab73858.png) + +This approach has the following benefits, + +1. Reduced ingestion latency - each burst commits entities which are processed before the entire list is processed. +2. Stable pressure - each period between bursts provides an opportunity for the processing pipeline to settle without overwhelming the pipeline with a large number of unprocessed entities. +3. Built-in retry / back-off - Failed bursts are automatically retried with a built-in back-off interval providing an opportunity for the data source to reset its rate limits before retrying the burst. +4. Prevents orphan entities - Deleted entities are removed as with `full` mutation with a low memory footprint. + +## Requirements + +The Incremental Entity Provider backend is designed for data sources that provide paginated results. Each burst attempts to handle one or more pages of the query. The plugin will attempt to fetch as many pages as it can within a configurable burst length. At every iteration, it expects to receive the next cursor that will be used to query in the next iteration. Each iteration may happen on a different replica. This has several consequences: + +1. The cursor must be serializable to JSON (not an issue for most RESTful or GraphQL based APIs). +2. The client must be stateless - a client is created from scratch for each iteration to allow distributing processing over multiple replicas. +3. There must be sufficient storage in Postgres to handle the additional data. (Presumably, this is also true of sqlite, but it has only been tested with Postgres.) + +## Installation + +1. Install `@backstage/plugin-catalog-backend-module-incremental-ingestion` with `yarn workspace backend add @backstage/plugin-catalog-backend-module-incremental-ingestion` +2. Import `IncrementalCatalogBuilder` from `@backstage/plugin-catalog-backend-module-incremental-ingestion` and instantiate it with `await IncrementalCatalogBuilder.create(env, builder)`. You have to pass `builder` into `IncrementalCatalogBuilder.create` function because `IncrementalCatalogBuilder` will convert an `IncrementalEntityProvider` into an `EntityProvider` and call `builder.addEntityProvider`. + + ```ts + const builder = CatalogBuilder.create(env); + // incremental builder receives builder because it'll register + // incremental entity providers with the builder + const incrementalBuilder = await IncrementalCatalogBuilder.create( + env, + builder, + ); + ``` + +3. Last step, add `await incrementBuilder.build()` after `await builder.build()` to ensure that all `CatalogBuilder` migration run before running `incrementBuilder.build()` migrations. + + ```ts + const { processingEngine, router } = await builder.build(); + + // this has to run after `await builder.build()` to ensure that catalog migrations are completed + // before incremental builder migrations are executed + await incrementalBuilder.build(); + ``` + + The result should look something like this, + + ```ts + import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; + import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; + import { IncrementalCatalogBuilder } from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; + import { Router } from 'express'; + import { Duration } from 'luxon'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = CatalogBuilder.create(env); + // incremental builder receives builder because it'll register + // incremental entity providers with the builder + const incrementalBuilder = await IncrementalCatalogBuilder.create( + env, + builder, + ); + + builder.addProcessor(new ScaffolderEntitiesProcessor()); + + const { processingEngine, router } = await builder.build(); + + // this has to run after `await builder.build()` so ensure that catalog migrations are completed + // before incremental builder migrations are executed + const { incrementalAdminRouter } = await incrementalBuilder.build(); + + router.use('/incremental', incrementalAdminRouter); + + await processingEngine.start(); + + return router; + } + ``` + +## Writing an Incremental Entity Provider + +To create an Incremental Entity Provider, you need to know how to retrieve a single page of the data that you wish to ingest into the Backstage catalog. If the API has pagination and you know how to make a paginated request to that API, you'll be able to implement an Incremental Entity Provider for this API. For more information about compatibility, checkout Compatible data sources section on this page. + +Here is the type definition for an Incremental Entity Provider. + +```ts +interface IncrementalEntityProvider { + /** + * This name must be unique between all of the entity providers + * operating in the catalog. + */ + getProviderName(): string; + + /** + * Do any setup and teardown necessary in order to provide the + * context for fetching pages. This should always invoke `burst` in + * order to fetch the individual pages. + * + * @param burst - a function which performs a series of iterations + */ + around(burst: (context: TContext) => Promise): Promise; + + /** + * Return a single page of entities from a specific point in the + * ingestion. + * + * @param context - anything needed in order to fetch a single page. + * @param cursor - a unique value identifying the page to ingest. + * @returns the entities to be ingested, as well as the cursor of + * the the next page after this one. + */ + next( + context: TContext, + cursor?: TCursor, + ): Promise>; +} +``` + +For tutorial, we'll write an Incremental Entity Provider that will call an imaginary API. This imaginary API will return a list of imaginary services. This imaginary API has an imaginary API client with the following interface. + +```ts +interface MyApiClient { + getServices(page: number): MyPaginatedResults; +} + +interface MyPaginatedResults { + items: T[]; + totalPages: number; +} + +interface Service { + name: string; +} +``` + +These are the only 3 methods that you need to implement. `getProviderName()` is pretty self explanatory and it's exactly same as on Entity Provider. + +```ts +import { + IncrementalEntityProvider, + EntityIteratorResult, +} from '@backstage/plugin-catalog-backend-module-incremental-ingestion'; + +// this will include your pagination information, let's say our API accepts a `page` parameter. +// In this case, the cursor will include `page` +interface MyApiCursor { + page: number; +} + +// This interface describes the type of data that will be passed to your burst function. +interface MyContext { + apiClient: MyApiClient; +} + +export class MyIncrementalEntityProvider + implements IncrementalEntityProvider +{ + getProviderName() { + return `MyIncrementalEntityProvider`; + } +} +``` + +`around` method is used for setup and tear-down. For example, if you need to create a client that will connect to the API, you would do that here. + +```ts +export class MyIncrementalEntityProvider + implements IncrementalEntityProvider +{ + getProviderName() { + return `MyIncrementalEntityProvider`; + } + + async around(burst: (context: MyContext) => Promise): Promise { + const apiClient = new MyApiClient(); + + await burst({ apiClient }); + + // if you need to do any teardown, you can do it here + } +} +``` + +If you need to pass a token to your API, then you can create a constructor that will receive a token and use the token to setup the client. + +```ts +export class MyIncrementalEntityProvider + implements IncrementalEntityProvider +{ + token: string; + + constructor(token: string) { + this.token = token; + } + + getProviderName() { + return `MyIncrementalEntityProvider`; + } + + async around(burst: (context: MyContext) => Promise): Promise { + const apiClient = new MyApiClient(this.token); + + await burst({ apiClient }); + } +} +``` + +The last step is to implement the actual `next` method that will accept the cursor, call the API, process the result and return the result. + +```ts +export class MyIncrementalEntityProvider implements IncrementalEntityProvider { + + token: string; + + constructor(token: string) { + this.token = token; + } + + getProviderName() { + return `MyIncrementalEntityProvider`; + } + + + async around(burst: (context: MyContext) => Promise): Promise { + + const apiClient = new MyApiClient(this.token) + + await burst({ apiClient }) + } + + async next(context: MyContext, cursor?: MyApiCursor = { page: 1 }): Promise> { + const { apiClient } = context; + + // call your API with the current cursor + const data = await apiClient.getServices(cursor); + + // calculate the next page + const nextPage = page + 1; + + // figure out if there are any more pages to fetch + const done = nextPage > data.totalPages; + + // convert returned items into entities + const entities = data.items.map(item => ({ + entity: { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: item.name, + annotations: { + // You need to define these, otherwise they'll fail validation + [ANNOTATION_LOCATION]: this.getProviderName(), + [ANNOTATION_ORIGIN_LOCATION]: this.getProviderName(), + } + } + spec: { + type: 'service' + } + } + })); + + // create the next cursor + const nextCursor = { + page: nextPage + }; + + return { + done, + entities, + cursor: nextCursor + } + } +} +``` + +Now that you have your new Incremental Entity Provider, we can connect it to the catalog. + +## Adding an Incremental Entity Provider to the catalog + +We'll assume you followed the Installation instructions. After you create your `incrementalBuilder`, you can instantiate your Entity Provider and pass it to the `addIncrementalEntityProvider` method. + +```ts +const incrementalBuilder = await IncrementalCatalogBuilder.create(env, builder); + +// I'm assuming you're going to get your token from config +const token = config.getString('myApiClient.token'); + +const myEntityProvider = new MyIncrementalEntityProvider(token) + +incrementalBuilder.addIncrementalEntityProvider( + myEntityProvider, + { + // how long should it attempt to read pages from the API + // keep this short. Incremental Entity Provider will attempt to + // read as many pages as it can in this time + burstLength: Duration.fromObject({ seconds: 3 }), + // how long should it wait between bursts? + burstInterval: Duration.fromObject({ seconds: 3 }), + // how long should it rest before re-ingesting again? + restLength: Duration.fromObject({ day: 1 }) + // optional back-off configuration - how long should it wait to retry? + backoff: [ + Duration.fromObject({ seconds: 5 }), + Duration.fromObject({ seconds: 30 }), + Duration.fromObject({ minutes: 10 }), + Duration.fromObject({ hours: 3 }) + ] + } +) +``` + +That's it!!! + +## Error handling + +If `around` or `next` methods throw an error, the error will show up in logs and it'll trigger the Incremental Entity Provider to try again after a back-off period. It'll keep trying until it reaches the last back-off attempt, at which point it will cancel the current ingestion and start over. You don't need to do anything special to handle the retry logic. diff --git a/plugins/catalog-backend-module-incremental-ingestion/api-report.md b/plugins/catalog-backend-module-incremental-ingestion/api-report.md new file mode 100644 index 0000000000..387e505c33 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/api-report.md @@ -0,0 +1,87 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-incremental-ingestion" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; +import type { Config } from '@backstage/config'; +import type { DeferredEntity } from '@backstage/plugin-catalog-backend'; +import type { DurationObjectUnits } from 'luxon'; +import type { Logger } from 'winston'; +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'; + +// @public +export type EntityIteratorResult = + | { + done: false; + entities: DeferredEntity[]; + cursor: T; + } + | { + done: true; + entities?: DeferredEntity[]; + cursor?: T; + }; + +// @public (undocumented) +export class IncrementalCatalogBuilder { + // (undocumented) + addIncrementalEntityProvider( + provider: IncrementalEntityProvider, + options: IncrementalEntityProviderOptions, + ): void; + // (undocumented) + build(): Promise<{ + incrementalAdminRouter: Router; + }>; + static create( + env: PluginEnvironment, + builder: CatalogBuilder, + ): Promise; +} + +// @public +export interface IncrementalEntityProvider { + around(burst: (context: TContext) => Promise): Promise; + getProviderName(): string; + next( + context: TContext, + cursor?: TCursor, + ): Promise>; +} + +// @public (undocumented) +export interface IncrementalEntityProviderOptions { + backoff?: DurationObjectUnits[]; + burstInterval: DurationObjectUnits; + burstLength: DurationObjectUnits; + rejectEmptySourceCollections?: boolean; + rejectRemovalsAbovePercentage?: number; + restLength: DurationObjectUnits; +} + +// @alpha +export const incrementalIngestionEntityProviderCatalogModule: (options: { + providers: { + provider: IncrementalEntityProvider; + options: IncrementalEntityProviderOptions; + }[]; +}) => BackendFeature; + +// @public (undocumented) +export type PluginEnvironment = { + logger: Logger; + database: PluginDatabaseManager; + scheduler: PluginTaskScheduler; + config: Config; + reader: UrlReader; + permissions: PermissionEvaluator; +}; +``` diff --git a/plugins/catalog-backend-module-incremental-ingestion/knexfile.js b/plugins/catalog-backend-module-incremental-ingestion/knexfile.js new file mode 100644 index 0000000000..4cf9ef77c3 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/knexfile.js @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This file makes it possible to run "yarn knex migrate:make some_file_name" +// to assist in making new migrations +module.exports = { + client: 'better-sqlite3', + connection: ':memory:', + useNullAsDefault: true, + migrations: { + directory: './migrations', + }, +}; diff --git a/plugins/catalog-backend-module-incremental-ingestion/migrations/20221116073152_init.js b/plugins/catalog-backend-module-incremental-ingestion/migrations/20221116073152_init.js new file mode 100644 index 0000000000..09b6af9004 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/migrations/20221116073152_init.js @@ -0,0 +1,179 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param { import("knex").Knex } knex + */ +exports.up = async function up(knex) { + /** + * Sets up the ingestions table + */ + await knex.schema.createTable('ingestions', table => { + table.comment('Tracks ingestion streams for very large data sets'); + + table + .uuid('id') + .notNullable() + .comment('Auto-generated ID of the ingestion'); + + table + .string('provider_name') + .notNullable() + .comment('each provider gets its own identifiable name'); + + table + .string('status') + .notNullable() + .comment( + 'One of "interstitial" | "bursting" | "backing off" | "resting" | "complete"', + ); + + table + .string('next_action') + .notNullable() + .comment("what will this, 'ingest', 'rest', 'backoff', 'nothing (done)'"); + + table + .timestamp('next_action_at') + .notNullable() + .defaultTo(knex.fn.now()) + .comment('the moment in time at which point ingestion can begin again'); + + table + .string('last_error') + .comment('records any error that occurred in the previous burst attempt'); + + table + .integer('attempts') + .notNullable() + .defaultTo(0) + .comment('how many attempts have been made to burst without success'); + + table + .timestamp('created_at') + .notNullable() + .defaultTo(knex.fn.now()) + .comment('when did this ingestion actually begin'); + + table + .timestamp('ingestion_completed_at') + .comment('when did the ingestion actually end'); + + table + .timestamp('rest_completed_at') + .comment('when did the rest period actually end'); + + table + .string('completion_ticket') + .notNullable() + .comment( + 'indicates whether the ticket is still open or stamped complete', + ); + }); + + await knex.schema.alterTable('ingestions', t => { + t.primary(['id']); + t.index('provider_name', 'ingestion_provider_name_idx'); + t.unique(['provider_name', 'completion_ticket'], { + indexName: 'ingestion_composite_index', + deferrable: 'deferred', + }); + }); + + /** + * Sets up the ingestion_marks table + */ + await knex.schema.createTable('ingestion_marks', table => { + table.comment('tracks each step of an iterative ingestion'); + + table + .uuid('id') + .notNullable() + .comment('Auto-generated ID of the ingestion mark'); + + table + .uuid('ingestion_id') + .notNullable() + .references('id') + .inTable('ingestions') + .onDelete('CASCADE') + .comment('The id of the ingestion in which this mark took place'); + + table + .json('cursor') + .comment( + 'the current data associated with this iteration wherever it is in this moment in time', + ); + + table + .integer('sequence') + .notNullable() + .defaultTo(0) + .comment('what is the order of this mark'); + + table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()); + }); + + await knex.schema.alterTable('ingestion_marks', t => { + t.primary(['id']); + t.index('ingestion_id', 'ingestion_mark_ingestion_id_idx'); + }); + + /** + * Set up the ingestion_mark_entities table + */ + await knex.schema.createTable('ingestion_mark_entities', table => { + table.comment( + 'tracks the entities recorded in each step of an iterative ingestion', + ); + + table + .uuid('id') + .notNullable() + .comment('Auto-generated ID of the marked entity'); + + table + .uuid('ingestion_mark_id') + .notNullable() + .references('id') + .inTable('ingestion_marks') + .onDelete('CASCADE') + .comment( + 'Every time a mark happens during an ingestion, there are a list of entities marked.', + ); + + table + .string('ref') + .notNullable() + .comment('the entity reference of the marked entity'); + }); + + await knex.schema.alterTable('ingestion_mark_entities', t => { + t.primary(['id']); + t.index('ingestion_mark_id', 'ingestion_mark_entity_ingestion_mark_id_idx'); + }); +}; + +/** + * @param { import("knex").Knex } knex + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('ingestion_mark_entities'); + await knex.schema.dropTable('ingestion_marks'); + await knex.schema.dropTable('ingestions'); +}; diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json new file mode 100644 index 0000000000..721dc7c69a --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -0,0 +1,67 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", + "description": "An entity provider for streaming large asset sources into the catalog", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-incremental-ingestion" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-catalog-backend": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", + "@backstage/plugin-permission-common": "workspace:^", + "@types/express": "^4.17.6", + "@types/luxon": "^3.0.0", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "knex": "^2.0.0", + "lodash": "^4.17.21", + "luxon": "^3.0.0", + "uuid": "^8.3.2", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-app-api": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-catalog-backend": "workspace:^", + "get-port": "^6.1.2" + }, + "files": [ + "alpha", + "dist", + "migrations/**/*.{js,d.ts}" + ] +} diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.ts b/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.ts new file mode 100644 index 0000000000..9eb52c4558 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/database/IncrementalIngestionDatabaseManager.ts @@ -0,0 +1,617 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Knex } from 'knex'; +import type { DeferredEntity } from '@backstage/plugin-catalog-backend'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { Duration } from 'luxon'; +import { v4 } from 'uuid'; +import { + IngestionRecord, + IngestionRecordUpdate, + IngestionUpsert, + MarkRecord, + MarkRecordInsert, +} from './tables'; + +export class IncrementalIngestionDatabaseManager { + private client: Knex; + + constructor(options: { client: Knex }) { + this.client = options.client; + } + + /** + * Performs an update to the ingestion record with matching `id`. + * @param options - IngestionRecordUpdate + */ + async updateIngestionRecordById(options: IngestionRecordUpdate) { + await this.client.transaction(async tx => { + const { ingestionId, update } = options; + await tx('ingestions').where('id', ingestionId).update(update); + }); + } + + /** + * Performs an update to the ingestion record with matching provider name. Will only update active records. + * @param provider - string + * @param update - Partial + */ + async updateIngestionRecordByProvider( + provider: string, + update: Partial, + ) { + await this.client.transaction(async tx => { + await tx('ingestions') + .where('provider_name', provider) + .andWhere('completion_ticket', 'open') + .update(update); + }); + } + + /** + * Performs an insert into the `ingestions` table with the supplied values. + * @param record - IngestionUpsertIFace + */ + async insertIngestionRecord(record: IngestionUpsert) { + await this.client.transaction(async tx => { + await tx('ingestions').insert(record); + }); + } + + private async deleteMarkEntities( + tx: Knex.Transaction, + ids: { id: string }[], + ) { + const chunks: { id: string }[][] = []; + for (let i = 0; i < ids.length; i += 100) { + const chunk = ids.slice(i, i + 100); + chunks.push(chunk); + } + + let deleted = 0; + + for (const chunk of chunks) { + const chunkDeleted = await tx('ingestion_mark_entities') + .delete() + .whereIn( + 'id', + chunk.map(entry => entry.id), + ); + deleted += chunkDeleted; + } + + return deleted; + } + + /** + * Finds the current ingestion record for the named provider. + * @param provider - string + * @returns IngestionRecord | undefined + */ + async getCurrentIngestionRecord(provider: string) { + return await this.client.transaction(async tx => { + const record = await tx('ingestions') + .where('provider_name', provider) + .andWhere('completion_ticket', 'open') + .first(); + return record; + }); + } + + /** + * Finds the last ingestion record for the named provider. + * @param provider - string + * @returns IngestionRecord | undefined + */ + async getPreviousIngestionRecord(provider: string) { + return await this.client.transaction(async tx => { + return await tx('ingestions') + .where('provider_name', provider) + .andWhereNot('completion_ticket', 'open') + .first(); + }); + } + + /** + * Removes all entries from `ingestion_marks_entities`, `ingestion_marks`, and `ingestions` + * for prior ingestions that completed (i.e., have a `completion_ticket` value other than 'open'). + * @param provider - string + * @returns A count of deletions for each record type. + */ + async clearFinishedIngestions(provider: string) { + return await this.client.transaction(async tx => { + const markEntitiesDeleted = await tx('ingestion_mark_entities') + .delete() + .whereIn( + 'ingestion_mark_id', + tx('ingestion_marks') + .select('id') + .whereIn( + 'ingestion_id', + tx('ingestions') + .select('id') + .where('provider_name', provider) + .andWhereNot('completion_ticket', 'open'), + ), + ); + + const marksDeleted = await tx('ingestion_marks') + .delete() + .whereIn( + 'ingestion_id', + tx('ingestions') + .select('id') + .where('provider_name', provider) + .andWhereNot('completion_ticket', 'open'), + ); + + const ingestionsDeleted = await tx('ingestions') + .delete() + .where('provider_name', provider) + .andWhereNot('completion_ticket', 'open'); + + return { + deletions: { + markEntitiesDeleted, + marksDeleted, + ingestionsDeleted, + }, + }; + }); + } + + /** + * Automatically cleans up duplicate ingestion records if they were accidentally created. + * Any ingestion record where the `rest_completed_at` is null (meaning it is active) AND + * the ingestionId is incorrect is a duplicate ingestion record. + * @param ingestionId - string + * @param provider - string + */ + async clearDuplicateIngestions(ingestionId: string, provider: string) { + await this.client.transaction(async tx => { + const invalid = await tx('ingestions') + .where('provider_name', provider) + .andWhere('rest_completed_at', null) + .andWhereNot('id', ingestionId); + + if (invalid.length > 0) { + await tx('ingestions').delete().whereIn('id', invalid); + await tx('ingestion_mark_entities') + .delete() + .whereIn( + 'ingestion_mark_id', + tx('ingestion_marks').select('id').whereIn('ingestion_id', invalid), + ); + await tx('ingestion_marks').delete().whereIn('ingestion_id', invalid); + } + }); + } + + /** + * This method fully purges and resets all ingestion records for the named provider, and + * leaves it in a paused state. + * @param provider - string + * @returns Counts of all deleted ingestion records + */ + async purgeAndResetProvider(provider: string) { + return await this.client.transaction(async tx => { + const ingestionIDs: { id: string }[] = await tx('ingestions') + .select('id') + .where('provider_name', provider); + + const markIDs: { id: string }[] = + ingestionIDs.length > 0 + ? await tx('ingestion_marks') + .select('id') + .whereIn( + 'ingestion_id', + ingestionIDs.map(entry => entry.id), + ) + : []; + + const markEntityIDs: { id: string }[] = + markIDs.length > 0 + ? await tx('ingestion_mark_entities') + .select('id') + .whereIn( + 'ingestion_mark_id', + markIDs.map(entry => entry.id), + ) + : []; + + const markEntitiesDeleted = await this.deleteMarkEntities( + tx, + markEntityIDs, + ); + + const marksDeleted = + markIDs.length > 0 + ? await tx('ingestion_marks') + .delete() + .whereIn( + 'ingestion_id', + ingestionIDs.map(entry => entry.id), + ) + : 0; + + const ingestionsDeleted = await tx('ingestions') + .delete() + .where('provider_name', provider); + + const next_action_at = new Date(); + next_action_at.setTime(next_action_at.getTime() + 24 * 60 * 60 * 1000); + + await this.insertIngestionRecord({ + id: v4(), + next_action: 'rest', + provider_name: provider, + next_action_at, + ingestion_completed_at: new Date(), + status: 'resting', + completion_ticket: 'open', + }); + + return { provider, ingestionsDeleted, marksDeleted, markEntitiesDeleted }; + }); + } + + /** + * Creates a new ingestion record. + * @param provider - string + * @returns A new ingestion record + */ + async createProviderIngestionRecord(provider: string) { + const ingestionId = v4(); + const nextAction = 'ingest'; + try { + await this.insertIngestionRecord({ + id: ingestionId, + next_action: nextAction, + provider_name: provider, + status: 'bursting', + completion_ticket: 'open', + }); + return { ingestionId, nextAction, attempts: 0, nextActionAt: Date.now() }; + } catch (_e) { + // Creating the ingestion record failed. Return undefined. + return undefined; + } + } + + /** + * Computes which entities to remove, if any, at the end of a burst. + * @param provider - string + * @param ingestionId - string + * @returns All entities to remove for this burst. + */ + async computeRemoved(provider: string, ingestionId: string) { + const previousIngestion = await this.getPreviousIngestionRecord(provider); + return await this.client.transaction(async tx => { + const count = await tx('ingestion_mark_entities') + .count({ total: 'ingestion_mark_entities.ref' }) + .join( + 'ingestion_marks', + 'ingestion_marks.id', + 'ingestion_mark_entities.ingestion_mark_id', + ) + .join('ingestions', 'ingestions.id', 'ingestion_marks.ingestion_id') + .where('ingestions.id', ingestionId); + + const total = count.reduce((acc, cur) => acc + (cur.total as number), 0); + + const removed: { entityRef: string }[] = []; + if (previousIngestion) { + const stale: { ref: string }[] = await tx('ingestion_mark_entities') + .select('ingestion_mark_entities.ref') + .join( + 'ingestion_marks', + 'ingestion_marks.id', + 'ingestion_mark_entities.ingestion_mark_id', + ) + .join('ingestions', 'ingestions.id', 'ingestion_marks.ingestion_id') + .where('ingestions.id', previousIngestion.id); + + removed.push( + ...stale.map(e => { + return { entityRef: e.ref }; + }), + ); + } + + return { total, removed }; + }); + } + + /** + * Performs a lookup of all providers that have duplicate active ingestion records. + * @returns An array of all duplicate active ingestions + */ + async healthcheck() { + return await this.client.transaction(async tx => { + const records = await tx<{ id: string; provider_name: string }>( + 'ingestions', + ) + .distinct('id', 'provider_name') + .where('rest_completed_at', null); + return records; + }); + } + + /** + * Skips any wait time for the next action to run. + * @param provider - string + */ + async triggerNextProviderAction(provider: string) { + await this.updateIngestionRecordByProvider(provider, { + next_action_at: new Date(), + }); + } + + /** + * Purges the following tables: + * * `ingestions` + * * `ingestion_marks` + * * `ingestion_mark_entities` + * + * This function leaves the ingestions table with all providers in a paused state. + * @returns Results from cleaning up all ingestion tables. + */ + async cleanupProviders() { + const providers = await this.listProviders(); + + const ingestionsDeleted = await this.purgeTable('ingestions'); + + const next_action_at = new Date(); + next_action_at.setTime(next_action_at.getTime() + 24 * 60 * 60 * 1000); + + for (const provider of providers) { + await this.insertIngestionRecord({ + id: v4(), + next_action: 'rest', + provider_name: provider, + next_action_at, + ingestion_completed_at: new Date(), + status: 'resting', + completion_ticket: 'open', + }); + } + + const ingestionMarksDeleted = await this.purgeTable('ingestion_marks'); + const markEntitiesDeleted = await this.purgeTable( + 'ingestion_mark_entities', + ); + + return { ingestionsDeleted, ingestionMarksDeleted, markEntitiesDeleted }; + } + + /** + * Configures the current ingestion record to ingest a burst. + * @param ingestionId - string + */ + async setProviderIngesting(ingestionId: string) { + await this.updateIngestionRecordById({ + ingestionId, + update: { next_action: 'ingest' }, + }); + } + + /** + * Indicates the provider is currently ingesting a burst. + * @param ingestionId - string + */ + async setProviderBursting(ingestionId: string) { + await this.updateIngestionRecordById({ + ingestionId, + update: { status: 'bursting' }, + }); + } + + /** + * Finalizes the current ingestion record to indicate that the post-ingestion rest period is complete. + * @param ingestionId - string + */ + async setProviderComplete(ingestionId: string) { + await this.updateIngestionRecordById({ + ingestionId, + update: { + next_action: 'nothing (done)', + rest_completed_at: new Date(), + status: 'complete', + completion_ticket: v4(), + }, + }); + } + + /** + * Marks ingestion as complete and starts the post-ingestion rest cycle. + * @param ingestionId - string + * @param restLength - Duration + */ + async setProviderResting(ingestionId: string, restLength: Duration) { + await this.updateIngestionRecordById({ + ingestionId, + update: { + next_action: 'rest', + next_action_at: new Date(Date.now() + restLength.as('milliseconds')), + ingestion_completed_at: new Date(), + status: 'resting', + }, + }); + } + + /** + * Marks ingestion as paused after a burst completes. + * @param ingestionId - string + */ + async setProviderInterstitial(ingestionId: string) { + await this.updateIngestionRecordById({ + ingestionId, + update: { attempts: 0, status: 'interstitial' }, + }); + } + + /** + * Starts the cancel process for the current ingestion. + * @param ingestionId - string + * @param message - string (optional) + */ + async setProviderCanceling(ingestionId: string, message?: string) { + const update: Partial = { + next_action: 'cancel', + last_error: message ? message : undefined, + next_action_at: new Date(), + status: 'canceling', + }; + await this.updateIngestionRecordById({ ingestionId, update }); + } + + /** + * Completes the cancel process and triggers a new ingestion. + * @param ingestionId - string + */ + async setProviderCanceled(ingestionId: string) { + await this.updateIngestionRecordById({ + ingestionId, + update: { + next_action: 'nothing (canceled)', + rest_completed_at: new Date(), + status: 'complete', + completion_ticket: v4(), + }, + }); + } + + /** + * Configures the current ingestion to wait and retry, due to a data source error. + * @param ingestionId - string + * @param attempts - number + * @param error - Error + * @param backoffLength - number + */ + async setProviderBackoff( + ingestionId: string, + attempts: number, + error: Error, + backoffLength: number, + ) { + await this.updateIngestionRecordById({ + ingestionId, + update: { + next_action: 'backoff', + attempts: attempts + 1, + last_error: String(error), + next_action_at: new Date(Date.now() + backoffLength), + status: 'backing off', + }, + }); + } + + /** + * Returns the last record from `ingestion_marks` for the supplied ingestionId. + * @param ingestionId - string + * @returns MarkRecord | undefined + */ + async getLastMark(ingestionId: string) { + return await this.client.transaction(async tx => { + const mark = await tx('ingestion_marks') + .where('ingestion_id', ingestionId) + .orderBy('sequence', 'desc') + .first(); + return mark; + }); + } + + async getAllMarks(ingestionId: string) { + return await this.client.transaction(async tx => { + const marks = await tx('ingestion_marks') + .where('ingestion_id', ingestionId) + .orderBy('sequence', 'desc'); + return marks; + }); + } + + /** + * Performs an insert into the `ingestion_marks` table with the supplied values. + * @param options - MarkRecordInsert + */ + async createMark(options: MarkRecordInsert) { + const { record } = options; + await this.client.transaction(async tx => { + await tx('ingestion_marks').insert(record); + }); + } + /** + * Performs an upsert to the `ingestion_mark_entities` table for all deferred entities. + * @param markId - string + * @param entities - DeferredEntity[] + */ + async createMarkEntities(markId: string, entities: DeferredEntity[]) { + const refs = entities.map(e => stringifyEntityRef(e.entity)); + + await this.client.transaction(async tx => { + const existingRefsArray = ( + await tx<{ ref: string }>('ingestion_mark_entities') + .select('ref') + .whereIn('ref', refs) + ).map(e => e.ref); + + const existingRefsSet = new Set(existingRefsArray); + + const newRefs = refs.filter(e => !existingRefsSet.has(e)); + + await tx('ingestion_mark_entities') + .update('ingestion_mark_id', markId) + .whereIn('ref', existingRefsArray); + + await tx('ingestion_mark_entities').insert( + newRefs.map(ref => ({ + id: v4(), + ingestion_mark_id: markId, + ref, + })), + ); + }); + } + + /** + * Deletes the entire content of a table, and returns the number of records deleted. + * @param table - string + * @returns number + */ + async purgeTable(table: string) { + return await this.client.transaction(async tx => { + return await tx(table).delete(); + }); + } + + /** + * Returns a list of all providers. + * @returns string[] + */ + async listProviders() { + return await this.client.transaction(async tx => { + const providers = await tx<{ provider_name: string }>( + 'ingestions', + ).distinct('provider_name'); + return providers.map(entry => entry.provider_name); + }); + } + + async updateByName(provider: string, update: Partial) { + await this.updateIngestionRecordByProvider(provider, update); + } +} diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/database/migrations.ts b/plugins/catalog-backend-module-incremental-ingestion/src/database/migrations.ts new file mode 100644 index 0000000000..940a82107b --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/database/migrations.ts @@ -0,0 +1,31 @@ +/* + * 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 { resolvePackagePath } from '@backstage/backend-common'; +import { Knex } from 'knex'; +import { DB_MIGRATIONS_TABLE } from './tables'; + +export async function applyDatabaseMigrations(knex: Knex): Promise { + const migrationsDir = resolvePackagePath( + '@backstage/plugin-catalog-backend-module-incremental-ingestion', + 'migrations', + ); + + await knex.migrate.latest({ + directory: migrationsDir, + tableName: DB_MIGRATIONS_TABLE, + }); +} diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/database/tables.ts b/plugins/catalog-backend-module-incremental-ingestion/src/database/tables.ts new file mode 100644 index 0000000000..00d40fb4a1 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/database/tables.ts @@ -0,0 +1,118 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const DB_MIGRATIONS_TABLE = 'incremental_ingestion__knex_migrations'; + +/** + * The shape of data inserted into or updated in the `ingestions` table. + */ +export interface IngestionUpsert { + /** + * The ingestion record id. + */ + id?: string; + /** + * The next action the incremental entity provider will take. + */ + next_action: + | 'rest' + | 'ingest' + | 'backoff' + | 'cancel' + | 'nothing (done)' + | 'nothing (canceled)'; + /** + * Current status of the incremental entity provider. + */ + status: + | 'complete' + | 'bursting' + | 'resting' + | 'canceling' + | 'interstitial' + | 'backing off'; + /** + * The name of the incremental entity provider being updated. + */ + provider_name: string; + /** + * Date/time stamp for when the next action will trigger. + */ + next_action_at?: Date; + /** + * A record of the last error generated by the incremental entity provider. + */ + last_error?: string | null; + /** + * The number of attempts the provider has attempted during the current cycle. + */ + attempts?: number; + /** + * Date/time stamp for the completion of ingestion. + */ + ingestion_completed_at?: Date | string | null; + /** + * Date/time stamp for the end of the rest cycle before the next ingestion. + */ + rest_completed_at?: Date | string | null; + /** + * A record of the finalized status of the ingestion record. Values are either 'open' or a uuid. + */ + completion_ticket: string; +} + +/** + * This interface is for updating an existing ingestion record. + */ +export interface IngestionRecordUpdate { + ingestionId: string; + update: Partial; +} + +/** + * The expected response from the `ingestion_marks` table. + */ +export interface MarkRecord { + id: string; + sequence: number; + ingestion_id: string; + cursor: string; + created_at: string; +} + +/** + * The expected response from the `ingestions` table. + */ +export interface IngestionRecord extends IngestionUpsert { + id: string; + next_action_at: Date; + /** + * The date/time the ingestion record was created. + */ + created_at: string; +} + +/** + * This interface supplies all the values for adding an ingestion mark. + */ +export interface MarkRecordInsert { + record: { + id: string; + ingestion_id: string; + cursor: unknown; + sequence: number; + }; +} diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts b/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts new file mode 100644 index 0000000000..e2000c609e --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/engine/IncrementalIngestionEngine.ts @@ -0,0 +1,329 @@ +/* + * 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 type { DeferredEntity } from '@backstage/plugin-catalog-backend'; +import { IterationEngine, IterationEngineOptions } from '../types'; +import { IncrementalIngestionDatabaseManager } from '../database/IncrementalIngestionDatabaseManager'; +import { performance } from 'perf_hooks'; +import { Duration, DurationObjectUnits } from 'luxon'; +import { v4 } from 'uuid'; +import { stringifyError } from '@backstage/errors'; + +export class IncrementalIngestionEngine implements IterationEngine { + private readonly restLength: Duration; + private readonly backoff: DurationObjectUnits[]; + + private manager: IncrementalIngestionDatabaseManager; + + constructor(private options: IterationEngineOptions) { + this.manager = options.manager; + this.restLength = Duration.fromObject(options.restLength); + this.backoff = options.backoff ?? [ + { minutes: 1 }, + { minutes: 5 }, + { minutes: 30 }, + { hours: 3 }, + ]; + } + + async taskFn(signal: AbortSignal) { + try { + this.options.logger.debug('Begin tick'); + await this.handleNextAction(signal); + } catch (error) { + this.options.logger.error(`${error}`); + throw error; + } finally { + this.options.logger.debug('End tick'); + } + } + + async handleNextAction(signal: AbortSignal) { + await this.options.ready; + + const result = await this.getCurrentAction(); + if (result) { + const { ingestionId, nextActionAt, nextAction, attempts } = result; + + switch (nextAction) { + case 'rest': + if (Date.now() > nextActionAt) { + await this.manager.clearFinishedIngestions( + this.options.provider.getProviderName(), + ); + this.options.logger.info( + `incremental-engine: Ingestion ${ingestionId} rest period complete. Ingestion will start again`, + ); + + await this.manager.setProviderComplete(ingestionId); + } else { + this.options.logger.info( + `incremental-engine: Ingestion '${ingestionId}' rest period continuing`, + ); + } + break; + case 'ingest': + try { + await this.manager.setProviderBursting(ingestionId); + const done = await this.ingestOneBurst(ingestionId, signal); + if (done) { + this.options.logger.info( + `incremental-engine: Ingestion '${ingestionId}' complete, transitioning to rest period of ${this.restLength.toHuman()}`, + ); + await this.manager.setProviderResting( + ingestionId, + this.restLength, + ); + } else { + await this.manager.setProviderInterstitial(ingestionId); + this.options.logger.info( + `incremental-engine: Ingestion '${ingestionId}' continuing`, + ); + } + } catch (error) { + if ( + (error as Error).message && + (error as Error).message === 'CANCEL' + ) { + this.options.logger.info( + `incremental-engine: Ingestion '${ingestionId}' canceled`, + ); + await this.manager.setProviderCanceling( + ingestionId, + (error as Error).message, + ); + } else { + const currentBackoff = Duration.fromObject( + this.backoff[Math.min(this.backoff.length - 1, attempts)], + ); + + const backoffLength = currentBackoff.as('milliseconds'); + this.options.logger.error(error); + + const truncatedError = stringifyError(error).substring(0, 700); + this.options.logger.error( + `incremental-engine: Ingestion '${ingestionId}' threw an error during ingestion burst. Ingestion will backoff for ${currentBackoff.toHuman()} (${truncatedError})`, + ); + + await this.manager.setProviderBackoff( + ingestionId, + attempts, + error as Error, + backoffLength, + ); + } + } + break; + case 'backoff': + if (Date.now() > nextActionAt) { + this.options.logger.info( + `incremental-engine: Ingestion '${ingestionId}' backoff complete, will attempt to resume`, + ); + await this.manager.setProviderIngesting(ingestionId); + } else { + this.options.logger.info( + `incremental-engine: Ingestion '${ingestionId}' backoff continuing`, + ); + } + break; + case 'cancel': + this.options.logger.info( + `incremental-engine: Ingestion '${ingestionId}' canceling, will restart`, + ); + await this.manager.setProviderCanceled(ingestionId); + break; + default: + this.options.logger.error( + `incremental-engine: Ingestion '${ingestionId}' received unknown action '${nextAction}'`, + ); + } + } else { + this.options.logger.error( + `incremental-engine: Engine tried to create duplicate ingestion record for provider '${this.options.provider.getProviderName()}'.`, + ); + } + } + + async getCurrentAction() { + const providerName = this.options.provider.getProviderName(); + const record = await this.manager.getCurrentIngestionRecord(providerName); + if (record) { + this.options.logger.info( + `incremental-engine: Ingestion record found: '${record.id}'`, + ); + return { + ingestionId: record.id, + nextAction: record.next_action as 'rest' | 'ingest' | 'backoff', + attempts: record.attempts as number, + nextActionAt: record.next_action_at.valueOf() as number, + }; + } + const result = await this.manager.createProviderIngestionRecord( + providerName, + ); + if (result) { + this.options.logger.info( + `incremental-engine: Ingestion record created: '${result.ingestionId}'`, + ); + } + return result; + } + + async ingestOneBurst(id: string, signal: AbortSignal) { + const lastMark = await this.manager.getLastMark(id); + + const cursor = lastMark ? lastMark.cursor : undefined; + let sequence = lastMark ? lastMark.sequence + 1 : 0; + + const start = performance.now(); + let count = 0; + let done = false; + this.options.logger.info( + `incremental-engine: Ingestion '${id}' burst initiated`, + ); + + await this.options.provider.around(async (context: unknown) => { + let next = await this.options.provider.next(context, cursor); + count++; + for (;;) { + done = next.done; + await this.mark({ + id, + sequence, + entities: next?.entities, + done: next.done, + cursor: next?.cursor, + }); + if (signal.aborted || next.done) { + break; + } else { + next = await this.options.provider.next(context, next.cursor); + count++; + sequence++; + } + } + }); + + this.options.logger.info( + `incremental-engine: Ingestion '${id}' burst complete. (${count} batches in ${Math.round( + performance.now() - start, + )}ms).`, + ); + return done; + } + + async mark(options: { + id: string; + sequence: number; + entities?: DeferredEntity[]; + done: boolean; + cursor?: unknown; + }) { + const { id, sequence, entities, done, cursor } = options; + this.options.logger.debug( + `incremental-engine: Ingestion '${id}': MARK ${ + entities ? entities.length : 0 + } entities, cursor: ${ + cursor ? JSON.stringify(cursor) : 'none' + }, done: ${done}`, + ); + const markId = v4(); + + await this.manager.createMark({ + record: { + id: markId, + ingestion_id: id, + cursor, + sequence, + }, + }); + + if (entities && entities.length > 0) { + await this.manager.createMarkEntities(markId, entities); + } + + const added = + entities?.map(deferred => ({ + ...deferred, + entity: { + ...deferred.entity, + metadata: { + ...deferred.entity.metadata, + annotations: { + ...deferred.entity.metadata.annotations, + }, + }, + }, + })) ?? []; + + const removed: { entityRef: string }[] = []; + + if (done) { + this.options.logger.info( + `incremental-engine: Ingestion '${id}': Final page reached, calculating removed entities`, + ); + const result = await this.manager.computeRemoved( + this.options.provider.getProviderName(), + id, + ); + + const { total } = result; + + let doRemoval = true; + if (this.options.rejectEmptySourceCollections) { + if (total === 0) { + this.options.logger.error( + `incremental-engine: Ingestion '${id}': Rejecting empty entity collection!`, + ); + doRemoval = false; + } + } + + if (this.options.rejectRemovalsAbovePercentage) { + // If the total entities upserted in this ingestion is 0, then + // 100% of entities are stale and marked for removal. + const percentRemoved = + total > 0 ? (result.removed.length / total) * 100 : 100; + if (percentRemoved <= this.options.rejectRemovalsAbovePercentage) { + this.options.logger.info( + `incremental-engine: Ingestion '${id}': Removing ${result.removed.length} entities that have no matching assets`, + ); + } else { + const notice = `Attempted to remove ${percentRemoved}% of matching entities!`; + this.options.logger.error( + `incremental-engine: Ingestion '${id}': ${notice}`, + ); + await this.manager.updateIngestionRecordById({ + ingestionId: id, + update: { + last_error: `REMOVAL_THRESHOLD exceeded on ingestion mark ${markId}: ${notice}`, + }, + }); + doRemoval = false; + } + } + if (doRemoval) { + removed.push(...result.removed); + } + } + + await this.options.connection.applyMutation({ + type: 'delta', + added, + removed, + }); + } +} diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/index.ts b/plugins/catalog-backend-module-incremental-ingestion/src/index.ts new file mode 100644 index 0000000000..976c91a1ce --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/index.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +/** + * Provides efficient incremental ingestion of entities into the catalog. + * + * @packageDocumentation + */ + +export * from './module'; +export * from './service'; +export { + type EntityIteratorResult, + type IncrementalEntityProvider, + type IncrementalEntityProviderOptions, + type PluginEnvironment, +} from './types'; diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts new file mode 100644 index 0000000000..0d910f664d --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.test.ts @@ -0,0 +1,113 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { IncrementalEntityProvider } from '../types'; +import { WrapperProviders } from './WrapperProviders'; + +describe('WrapperProviders', () => { + const applyDatabaseMigrations = jest.fn(); + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + const config = new ConfigReader({}); + const logger = getVoidLogger(); + const scheduler = { + scheduleTask: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it.each(databases.eachSupportedId())( + 'should initialize the providers in order, %p', + async databaseId => { + const client = await databases.init(databaseId); + + const provider1: IncrementalEntityProvider = { + getProviderName: () => 'provider1', + around: burst => burst(0), + next: async (_context, cursor) => { + return !cursor + ? { done: false, entities: [], cursor: 1 } + : { done: true }; + }, + }; + + const provider2: IncrementalEntityProvider = { + getProviderName: () => 'provider2', + around: burst => burst(0), + next: async (_context, cursor) => { + return !cursor + ? { done: false, entities: [], cursor: 1 } + : { done: true }; + }, + }; + + const providers = new WrapperProviders({ + config, + logger, + client, + scheduler: + scheduler as Partial as PluginTaskScheduler, + applyDatabaseMigrations, + }); + const wrapped1 = providers.wrap(provider1, { + burstInterval: { seconds: 1 }, + burstLength: { seconds: 1 }, + restLength: { seconds: 1 }, + }); + const wrapped2 = providers.wrap(provider2, { + burstInterval: { seconds: 1 }, + burstLength: { seconds: 1 }, + restLength: { seconds: 1 }, + }); + + let resolved = false; + (providers as any).readySignal.then(() => { + resolved = true; + }); + + expect(applyDatabaseMigrations).toHaveBeenCalledTimes(0); + expect(resolved).toBe(false); + expect(scheduler.scheduleTask).not.toHaveBeenCalled(); + + await wrapped1.connect({} as any); // simulates the catalog engine + + expect(resolved).toBe(false); + expect(applyDatabaseMigrations).toHaveBeenCalledTimes(1); + expect(scheduler.scheduleTask).toHaveBeenLastCalledWith( + expect.objectContaining({ + id: 'provider1', + }), + ); + + await wrapped2.connect({} as any); + + expect(resolved).toBe(true); + expect(applyDatabaseMigrations).toHaveBeenCalledTimes(1); + expect(scheduler.scheduleTask).toHaveBeenLastCalledWith( + expect.objectContaining({ + id: 'provider2', + }), + ); + }, + ); +}); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.ts new file mode 100644 index 0000000000..44da5067fd --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/WrapperProviders.ts @@ -0,0 +1,142 @@ +/* + * 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 { Logger, loggerToWinstonLogger } from '@backstage/backend-plugin-api'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { Config } from '@backstage/config'; +import { stringifyError } from '@backstage/errors'; +import { + EntityProvider, + EntityProviderConnection, +} from '@backstage/plugin-catalog-node'; +import express from 'express'; +import { Knex } from 'knex'; +import { Duration } from 'luxon'; +import { IncrementalIngestionDatabaseManager } from '../database/IncrementalIngestionDatabaseManager'; +import { applyDatabaseMigrations } from '../database/migrations'; +import { IncrementalIngestionEngine } from '../engine/IncrementalIngestionEngine'; +import { createIncrementalProviderRouter } from '../router/routes'; +import { + IncrementalEntityProvider, + IncrementalEntityProviderOptions, +} from '../types'; +import { Deferred } from '../util'; + +/** + * Helps in the creation of the catalog entity providers that wrap the + * incremental ones. + */ +export class WrapperProviders { + private migrate: Promise | undefined; + private numberOfProvidersToConnect = 0; + private readonly readySignal = new Deferred(); + + constructor( + private readonly options: { + config: Config; + logger: Logger; + client: Knex; + scheduler: PluginTaskScheduler; + applyDatabaseMigrations?: typeof applyDatabaseMigrations; + }, + ) {} + + wrap( + provider: IncrementalEntityProvider, + options: IncrementalEntityProviderOptions, + ): EntityProvider { + this.numberOfProvidersToConnect += 1; + return { + getProviderName: () => provider.getProviderName(), + connect: async connection => { + await this.startProvider(provider, options, connection); + this.numberOfProvidersToConnect -= 1; + if (this.numberOfProvidersToConnect === 0) { + this.readySignal.resolve(); + } + }, + }; + } + + async adminRouter(): Promise { + return createIncrementalProviderRouter( + new IncrementalIngestionDatabaseManager({ client: this.options.client }), + loggerToWinstonLogger(this.options.logger), + ); + } + + private async startProvider( + provider: IncrementalEntityProvider, + providerOptions: IncrementalEntityProviderOptions, + connection: EntityProviderConnection, + ) { + const logger = loggerToWinstonLogger( + this.options.logger.child({ + entityProvider: provider.getProviderName(), + }), + ); + + try { + if (!this.migrate) { + this.migrate = Promise.resolve().then(async () => { + const apply = + this.options.applyDatabaseMigrations ?? applyDatabaseMigrations; + await apply(this.options.client); + }); + } + + await this.migrate; + + const { burstInterval, burstLength, restLength } = providerOptions; + + logger.info(`Connecting`); + + const manager = new IncrementalIngestionDatabaseManager({ + client: this.options.client, + }); + const engine = new IncrementalIngestionEngine({ + ...providerOptions, + ready: this.readySignal, + manager, + logger, + provider, + restLength, + connection, + }); + + const frequency = Duration.isDuration(burstInterval) + ? burstInterval + : Duration.fromObject(burstInterval); + const length = Duration.isDuration(burstLength) + ? burstLength + : Duration.fromObject(burstLength); + + await this.options.scheduler.scheduleTask({ + id: provider.getProviderName(), + fn: engine.taskFn.bind(engine), + frequency, + timeout: length, + }); + } catch (error) { + logger.warn( + `Failed to initialize incremental ingestion provider ${provider.getProviderName()}, ${stringifyError( + error, + )}`, + ); + throw error; + } + } +} diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.test.ts new file mode 100644 index 0000000000..06cbcb010e --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { coreServices } from '@backstage/backend-plugin-api'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { IncrementalEntityProvider } from '../types'; +import { incrementalIngestionEntityProviderCatalogModule } from './incrementalIngestionEntityProviderCatalogModule'; + +describe('bitbucketServerEntityProviderCatalogModule', () => { + it('should register provider at the catalog extension point', async () => { + const provider1: IncrementalEntityProvider = { + getProviderName: () => 'provider1', + around: burst => burst(0), + next: async (cursor, _context) => { + return !cursor + ? { done: false, entities: [], cursor: 1 } + : { done: true }; + }, + }; + + const addEntityProvider = jest.fn(); + const httpRouterUse = jest.fn(); + + const scheduler = {}; + const database = { + getClient: jest.fn(), + }; + const httpRouter = { + use: httpRouterUse, + }; + + await startTestBackend({ + extensionPoints: [ + [catalogProcessingExtensionPoint, { addEntityProvider }], + ], + services: [ + [coreServices.config, new ConfigReader({})], + [coreServices.database, database], + [coreServices.httpRouter, httpRouter], + [coreServices.logger, getVoidLogger()], + [coreServices.scheduler, scheduler], + ], + features: [ + incrementalIngestionEntityProviderCatalogModule({ + providers: [ + { + provider: provider1, + options: { + burstInterval: { seconds: 1 }, + burstLength: { seconds: 1 }, + restLength: { seconds: 1 }, + }, + }, + ], + }), + ], + }); + + expect(addEntityProvider).toHaveBeenCalledTimes(1); + expect(addEntityProvider.mock.calls[0][0].getProviderName()).toBe( + 'provider1', + ); + expect(httpRouterUse).toHaveBeenCalledTimes(1); + }); +}); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.ts new file mode 100644 index 0000000000..0ecb45292c --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/incrementalIngestionEntityProviderCatalogModule.ts @@ -0,0 +1,81 @@ +/* + * 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, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; +import { + IncrementalEntityProvider, + IncrementalEntityProviderOptions, +} from '../types'; +import { WrapperProviders } from './WrapperProviders'; + +/** + * Registers the incremental entity provider with the catalog processing extension point. + * + * @alpha + */ +export const incrementalIngestionEntityProviderCatalogModule = + createBackendModule({ + pluginId: 'catalog', + moduleId: 'incrementalIngestionEntityProvider', + register( + env, + options: { + providers: Array<{ + provider: IncrementalEntityProvider; + options: IncrementalEntityProviderOptions; + }>; + }, + ) { + env.registerInit({ + deps: { + catalog: catalogProcessingExtensionPoint, + config: coreServices.config, + database: coreServices.database, + httpRouter: coreServices.httpRouter, + logger: coreServices.logger, + scheduler: coreServices.scheduler, + }, + async init({ + catalog, + config, + database, + httpRouter, + logger, + scheduler, + }) { + const client = await database.getClient(); + + const providers = new WrapperProviders({ + config, + logger, + client, + scheduler, + }); + + for (const entry of options.providers) { + const wrapped = providers.wrap(entry.provider, entry.options); + catalog.addEntityProvider(wrapped); + } + + httpRouter.use(await providers.adminRouter()); + }, + }); + }, + }); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/index.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/index.ts new file mode 100644 index 0000000000..577cf193e2 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { incrementalIngestionEntityProviderCatalogModule } from './incrementalIngestionEntityProviderCatalogModule'; diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/router/paths.ts b/plugins/catalog-backend-module-incremental-ingestion/src/router/paths.ts new file mode 100644 index 0000000000..2118177821 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/router/paths.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const PROVIDER_CLEANUP = '/incremental/cleanup'; +export const PROVIDER_HEALTH = '/incremental/health'; +export const PROVIDER_BASE_PATH = '/incremental/providers/:provider'; diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/router/routes.ts b/plugins/catalog-backend-module-incremental-ingestion/src/router/routes.ts new file mode 100644 index 0000000000..9f480e879e --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/router/routes.ts @@ -0,0 +1,232 @@ +/* + * 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 { errorHandler } from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { IncrementalIngestionDatabaseManager } from '../database/IncrementalIngestionDatabaseManager'; +import { PROVIDER_BASE_PATH, PROVIDER_CLEANUP, PROVIDER_HEALTH } from './paths'; + +export const createIncrementalProviderRouter = async ( + manager: IncrementalIngestionDatabaseManager, + logger: Logger, +) => { + const router = Router(); + router.use(express.json()); + + // Get the overall health of all incremental providers + router.get(PROVIDER_HEALTH, async (_, res) => { + const records = await manager.healthcheck(); + const providers = records.map(record => record.provider_name); + const duplicates = [ + ...new Set(providers.filter((e, i, a) => a.indexOf(e) !== i)), + ]; + + if (duplicates.length > 0) { + res.json({ healthy: false, duplicateIngestions: duplicates }); + } else { + res.json({ healthy: true }); + } + }); + + // Clean up and pause all providers + router.post(PROVIDER_CLEANUP, async (_, res) => { + const result = await manager.cleanupProviders(); + res.json(result); + }); + + // Get basic status of the provider + router.get(PROVIDER_BASE_PATH, async (req, res) => { + const { provider } = req.params; + const record = await manager.getCurrentIngestionRecord(provider); + if (record) { + res.json({ + success: true, + status: { + current_action: record.status, + next_action_at: new Date(record.next_action_at), + }, + last_error: record.last_error, + }); + } else { + const providers: string[] = await manager.listProviders(); + if (providers.includes(provider)) { + res.json({ + success: true, + status: { + current_action: 'rest complete, waiting to start', + }, + }); + } else { + logger.error( + `${provider} - No ingestion record found in the database!`, + ); + res.status(404).json({ + success: false, + status: {}, + last_error: `Provider '${provider}' not found`, + }); + } + } + }); + + // Trigger the provider's next action + router.post(`${PROVIDER_BASE_PATH}/trigger`, async (req, res) => { + const { provider } = req.params; + const record = await manager.getCurrentIngestionRecord(provider); + if (record) { + await manager.triggerNextProviderAction(provider); + res.json({ + success: true, + message: `${provider}: Next action triggered.`, + }); + } else { + const providers: string[] = await manager.listProviders(); + if (providers.includes(provider)) { + logger.debug(`${provider} - Ingestion record found`); + res.json({ + success: true, + message: 'Unable to trigger next action (provider is restarting)', + }); + } else { + res.status(404).json({ + success: false, + message: `Provider '${provider}' not found`, + }); + } + } + }); + + // Start a brand-new ingestion cycle for the provider. + // (Cancel's the current run if active, or marks it complete if resting) + router.post(`${PROVIDER_BASE_PATH}/start`, async (req, res) => { + const { provider } = req.params; + + const record = await manager.getCurrentIngestionRecord(provider); + if (record) { + const ingestionId = record.id; + if (record.status === 'resting') { + await manager.setProviderComplete(ingestionId); + } else { + await manager.setProviderCanceling(ingestionId); + } + res.json({ + success: true, + message: `${provider}: Next cycle triggered.`, + }); + } else { + const providers: string[] = await manager.listProviders(); + if (providers.includes(provider)) { + logger.debug(`${provider} - Ingestion record found`); + res.json({ + success: true, + message: 'Provider is already restarting', + }); + } else { + res.status(404).json({ + success: false, + message: `Provider '${provider}' not found`, + }); + } + } + }); + + // Stop the provider and pause it for 24 hours + router.post(`${PROVIDER_BASE_PATH}/cancel`, async (req, res) => { + const { provider } = req.params; + const record = await manager.getCurrentIngestionRecord(provider); + if (record) { + const next_action_at = new Date(); + next_action_at.setTime(next_action_at.getTime() + 24 * 60 * 60 * 1000); + await manager.updateByName(provider, { + next_action: 'nothing (done)', + ingestion_completed_at: new Date(), + next_action_at, + status: 'resting', + }); + res.json({ + success: true, + message: `${provider}: Current ingestion canceled.`, + }); + } else { + const providers: string[] = await manager.listProviders(); + if (providers.includes(provider)) { + logger.debug(`${provider} - Ingestion record found`); + res.json({ + success: true, + message: 'Provider is currently restarting, please wait.', + }); + } else { + res.status(404).json({ + success: false, + message: `Provider '${provider}' not found`, + }); + } + } + }); + + // Wipe out all ingestion records for the provider and pause for 24 hours + router.delete(PROVIDER_BASE_PATH, async (req, res) => { + const { provider } = req.params; + const result = await manager.purgeAndResetProvider(provider); + res.json(result); + }); + + // Get the ingestion marks for the current cycle + router.get(`${PROVIDER_BASE_PATH}/marks`, async (req, res) => { + const { provider } = req.params; + const record = await manager.getCurrentIngestionRecord(provider); + if (record) { + const id = record.id; + const records = await manager.getAllMarks(id); + res.json({ success: true, records }); + } else { + const providers: string[] = await manager.listProviders(); + if (providers.includes(provider)) { + logger.debug(`${provider} - Ingestion record found`); + res.json({ + success: true, + message: 'No records yet (provider is restarting)', + }); + } else { + logger.error( + `${provider} - No ingestion record found in the database!`, + ); + res.status(404).json({ + success: false, + status: {}, + last_error: `Provider '${provider}' not found`, + }); + } + } + }); + + router.delete(`${PROVIDER_BASE_PATH}/marks`, async (req, res) => { + const { provider } = req.params; + const deletions = await manager.clearFinishedIngestions(provider); + + res.json({ + success: true, + message: `Expired marks for provider '${provider}' removed.`, + deletions, + }); + }); + + router.use(errorHandler()); + + return router; +}; diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/run.ts b/plugins/catalog-backend-module-incremental-ingestion/src/run.ts new file mode 100644 index 0000000000..08d95f4417 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/run.ts @@ -0,0 +1,99 @@ +/* + * 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 import/no-extraneous-dependencies +import { + databaseFactory, + discoveryFactory, + httpRouterFactory, + lifecycleFactory, + loggerFactory, + permissionsFactory, + rootLoggerFactory, + schedulerFactory, + tokenManagerFactory, + urlReaderFactory, +} from '@backstage/backend-app-api'; +import { coreServices } from '@backstage/backend-plugin-api'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { catalogPlugin } from '@backstage/plugin-catalog-backend'; +import { + IncrementalEntityProvider, + incrementalIngestionEntityProviderCatalogModule, +} from '.'; + +const provider: IncrementalEntityProvider = { + getProviderName: () => 'test-provider', + around: burst => burst(0), + next: async (_context, cursor) => { + await new Promise(resolve => setTimeout(resolve, 500)); + if (cursor === undefined || cursor < 3) { + console.log(`### Returning batch #${cursor}`); + return { done: false, entities: [], cursor: (cursor ?? 0) + 1 }; + } + + console.log('### Last batch reached, stopping'); + return { done: true }; + }, +}; + +async function main() { + const config = { + backend: { + baseUrl: 'http://localhost:7007', + listen: ':7007', + database: { client: 'better-sqlite3', connection: ':memory:' }, + }, + }; + + await startTestBackend({ + services: [ + [coreServices.config, new ConfigReader(config)], + databaseFactory(), + discoveryFactory(), + httpRouterFactory(), + lifecycleFactory(), + loggerFactory(), + permissionsFactory(), + rootLoggerFactory(), + schedulerFactory(), + tokenManagerFactory(), + urlReaderFactory(), + ], + extensionPoints: [], + features: [ + catalogPlugin(), + incrementalIngestionEntityProviderCatalogModule({ + providers: [ + { + provider: provider, + options: { + burstInterval: { seconds: 1 }, + burstLength: { seconds: 10 }, + restLength: { seconds: 10 }, + }, + }, + ], + }), + ], + }); +} + +main().catch(error => { + console.error(error.stack); + process.exit(1); +}); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/service/IncrementalCatalogBuilder.ts b/plugins/catalog-backend-module-incremental-ingestion/src/service/IncrementalCatalogBuilder.ts new file mode 100644 index 0000000000..d6bc5549aa --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/service/IncrementalCatalogBuilder.ts @@ -0,0 +1,116 @@ +/* + * 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 { + IncrementalEntityProvider, + IncrementalEntityProviderOptions, + PluginEnvironment, +} from '../types'; +import { CatalogBuilder as CoreCatalogBuilder } from '@backstage/plugin-catalog-backend'; +import { Duration } from 'luxon'; +import { Knex } from 'knex'; +import { IncrementalIngestionEngine } from '../engine/IncrementalIngestionEngine'; +import { applyDatabaseMigrations } from '../database/migrations'; +import { IncrementalIngestionDatabaseManager } from '../database/IncrementalIngestionDatabaseManager'; +import { createIncrementalProviderRouter } from '../router/routes'; +import { Deferred } from '../util'; + +/** @public */ +export class IncrementalCatalogBuilder { + /** + * Creates the incremental catalog builder, which extends the regular catalog builder. + * @param env - PluginEnvironment + * @param builder - CatalogBuilder + * @returns IncrementalCatalogBuilder + */ + static async create(env: PluginEnvironment, builder: CoreCatalogBuilder) { + const client = await env.database.getClient(); + const manager = new IncrementalIngestionDatabaseManager({ client }); + return new IncrementalCatalogBuilder(env, builder, client, manager); + } + + private ready: Deferred; + + private constructor( + private env: PluginEnvironment, + private builder: CoreCatalogBuilder, + private client: Knex, + private manager: IncrementalIngestionDatabaseManager, + ) { + this.ready = new Deferred(); + } + + async build() { + await applyDatabaseMigrations(this.client); + this.ready.resolve(); + + const routerLogger = this.env.logger.child({ + router: 'IncrementalProviderAdmin', + }); + + const incrementalAdminRouter = await createIncrementalProviderRouter( + this.manager, + routerLogger, + ); + + return { incrementalAdminRouter }; + } + + addIncrementalEntityProvider( + provider: IncrementalEntityProvider, + options: IncrementalEntityProviderOptions, + ) { + const { burstInterval, burstLength, restLength } = options; + const { logger: catalogLogger, scheduler } = this.env; + const ready = this.ready; + + const manager = this.manager; + + this.builder.addEntityProvider({ + getProviderName: provider.getProviderName.bind(provider), + async connect(connection) { + const logger = catalogLogger.child({ + entityProvider: provider.getProviderName(), + }); + + logger.info(`Connecting`); + + const engine = new IncrementalIngestionEngine({ + ...options, + ready, + manager, + logger, + provider, + restLength, + connection, + }); + + const frequency = Duration.isDuration(burstInterval) + ? burstInterval + : Duration.fromObject(burstInterval); + const length = Duration.isDuration(burstLength) + ? burstLength + : Duration.fromObject(burstLength); + + await scheduler.scheduleTask({ + id: provider.getProviderName(), + fn: engine.taskFn.bind(engine), + frequency, + timeout: length, + }); + }, + }); + } +} diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/service/index.ts b/plugins/catalog-backend-module-incremental-ingestion/src/service/index.ts new file mode 100644 index 0000000000..5df92feed5 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/service/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { IncrementalCatalogBuilder } from './IncrementalCatalogBuilder'; diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/types.ts b/plugins/catalog-backend-module-incremental-ingestion/src/types.ts new file mode 100644 index 0000000000..51a8738500 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/types.ts @@ -0,0 +1,167 @@ +/* + * 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 type { + PluginDatabaseManager, + UrlReader, +} from '@backstage/backend-common'; +import type { + PluginTaskScheduler, + TaskFunction, +} from '@backstage/backend-tasks'; +import type { Config } from '@backstage/config'; +import type { + DeferredEntity, + EntityProviderConnection, +} from '@backstage/plugin-catalog-backend'; +import type { PermissionEvaluator } from '@backstage/plugin-permission-common'; +import type { DurationObjectUnits } from 'luxon'; +import type { Logger } from 'winston'; +import { IncrementalIngestionDatabaseManager } from './database/IncrementalIngestionDatabaseManager'; + +/** + * Ingest entities into the catalog in bite-sized chunks. + * + * A Normal `EntityProvider` allows you to introduce entities into the + * processing pipeline by calling an `applyMutation()` on the full set + * of entities. However, this is not great when the number of entities + * that you have to keep track of is extremely large because it + * entails having all of them in memory at once. An + * `IncrementalEntityProvider` by contrast allows you to provide + * batches of entities in sequence so that you never need to have more + * than a few hundred in memory at a time. + * + * @public + */ +export interface IncrementalEntityProvider { + /** + * This name must be unique between all of the entity providers + * operating in the catalog. + */ + getProviderName(): string; + + /** + * Return a single page of entities from a specific point in the + * ingestion. + * + * @param context - anything needed in order to fetch a single page. + * @param cursor - a unique value identifying the page to ingest. + * @returns The entities to be ingested, as well as the cursor of + * the next page after this one. + */ + next( + context: TContext, + cursor?: TCursor, + ): Promise>; + + /** + * Do any setup and teardown necessary in order to provide the + * context for fetching pages. This should always invoke `burst` in + * order to fetch the individual pages. + * + * @param burst - a function which performs a series of iterations + */ + around(burst: (context: TContext) => Promise): Promise; +} + +/** + * Value returned by an {@link IncrementalEntityProvider} to provide a + * single page of entities to ingest. + * + * @public + */ +export type EntityIteratorResult = + | { + done: false; + entities: DeferredEntity[]; + cursor: T; + } + | { + done: true; + entities?: DeferredEntity[]; + cursor?: T; + }; + +/** @public */ +export interface IncrementalEntityProviderOptions { + /** + * Entities are ingested in bursts. This interval determines how + * much time to wait in between each burst. + */ + burstInterval: DurationObjectUnits; + + /** + * Entities are ingested in bursts. This value determines how long + * to keep ingesting within each burst. + */ + burstLength: DurationObjectUnits; + + /** + * After a successful ingestion, the incremental entity provider + * will rest for this period of time before starting to ingest + * again. + */ + restLength: DurationObjectUnits; + + /** + * In the event of an error during an ingestion burst, the backoff + * determines how soon it will be retried. E.g. + * `[{ minutes: 1}, { minutes: 5}, {minutes: 30 }, { hours: 3 }]` + */ + backoff?: DurationObjectUnits[]; + + /** + * If an error occurs at a data source that results in a large + * number of assets being inadvertently removed, it will result in + * Backstage removing all associated entities. To avoid that, set + * a percentage of entities past which removal will be disallowed. + */ + rejectRemovalsAbovePercentage?: number; + + /** + * Similar to the rejectRemovalsAbovePercentage, this option + * prevents removals in circumstances where a data source has + * improperly returned 0 assets. If set to `true`, Backstage will + * reject removals when that happens. + */ + rejectEmptySourceCollections?: boolean; +} + +/** @public */ +export type PluginEnvironment = { + logger: Logger; + database: PluginDatabaseManager; + scheduler: PluginTaskScheduler; + config: Config; + reader: UrlReader; + permissions: PermissionEvaluator; +}; + +export interface IterationEngine { + taskFn: TaskFunction; +} + +export interface IterationEngineOptions { + logger: Logger; + connection: EntityProviderConnection; + manager: IncrementalIngestionDatabaseManager; + provider: IncrementalEntityProvider; + restLength: DurationObjectUnits; + ready: Promise; + backoff?: IncrementalEntityProviderOptions['backoff']; + rejectRemovalsAbovePercentage?: number; + rejectEmptySourceCollections?: boolean; +} diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/util.ts b/plugins/catalog-backend-module-incremental-ingestion/src/util.ts new file mode 100644 index 0000000000..d9e820f826 --- /dev/null +++ b/plugins/catalog-backend-module-incremental-ingestion/src/util.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class Deferred implements Promise { + #resolve?: (value: T) => void; + #reject?: (error: Error) => void; + + public get resolve() { + return this.#resolve!; + } + public get reject() { + return this.#reject!; + } + + public then: Promise['then']; + public catch: Promise['catch']; + public finally: Promise['finally']; + + public constructor() { + const promise = new Promise((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; + }); + + this.then = promise.then.bind(promise); + this.catch = promise.catch.bind(promise); + this.finally = promise.finally.bind(promise); + } + + [Symbol.toStringTag]: 'Deferred' = 'Deferred'; +} diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 8a58fce1f5..26d07f3e42 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.5.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.5.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.5.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + ## 0.5.5-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index d095be6b5c..c8eb6f3e6c 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend module that helps integrate towards LDAP", - "version": "0.5.5-next.0", + "version": "0.5.6-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 8b8d4479d3..86be820832 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## 0.4.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.4.4 + +### Patch Changes + +- 0e37858f22: Added cause information to logged warnings +- 8d1a5e08ca: `MicrosoftGraphOrgEntityProvider`: Add option to configure schedule via `app-config.yaml` instead of in code. + + Please find how to configure the schedule at the config at + https://github.com/backstage/backstage/tree/master/plugins/catalog-backend-module-msgraph#readme + +- 384f99c276: Add `microsoftGraphOrgEntityProviderCatalogModule` (new backend-plugin-api, alpha). +- Updated dependencies + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + +## 0.4.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + ## 0.4.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index cf301c71a0..42be4c88fe 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", - "version": "0.4.4-next.0", + "version": "0.4.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -55,7 +55,7 @@ "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", "luxon": "^3.0.0", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "alpha", diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 8e1d9a3776..0ed411bfea 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -122,7 +122,7 @@ export async function readMicrosoftGraphUsers( 120, ); } catch (e) { - options.logger.warn(`Unable to load photo for ${user.id}`); + options.logger.warn(`Unable to load photo for ${user.id}, ${e}`); } const entity = await transformer(user, userPhoto); @@ -206,7 +206,7 @@ export async function readMicrosoftGraphUsersInGroups( expand: options.userExpand, }); } catch (e) { - options.logger.warn(`Unable to load user for ${userId}`); + options.logger.warn(`Unable to load user for ${userId}, ${e}`); } if (user) { try { @@ -217,7 +217,7 @@ export async function readMicrosoftGraphUsersInGroups( 120, ); } catch (e) { - options.logger.warn(`Unable to load userphoto for ${userId}`); + options.logger.warn(`Unable to load userphoto for ${userId}, ${e}`); } const entity = await transformer(user, userPhoto); diff --git a/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.test.ts b/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.test.ts index 764f504898..27ae5b9dd8 100644 --- a/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.test.ts @@ -15,11 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { - configServiceRef, - loggerServiceRef, - schedulerServiceRef, -} from '@backstage/backend-plugin-api'; +import { coreServices } from '@backstage/backend-plugin-api'; import { PluginTaskScheduler, TaskScheduleDefinition, @@ -71,9 +67,9 @@ describe('awsS3EntityProviderCatalogModule', () => { await startTestBackend({ extensionPoints: [[catalogProcessingExtensionPoint, extensionPoint]], services: [ - [configServiceRef, config], - [loggerServiceRef, getVoidLogger()], - [schedulerServiceRef, scheduler], + [coreServices.config, config], + [coreServices.logger, getVoidLogger()], + [coreServices.scheduler, scheduler], ], features: [microsoftGraphOrgEntityProviderCatalogModule()], }); diff --git a/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.ts b/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.ts index 9e817636fa..2643e6e937 100644 --- a/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.ts +++ b/plugins/catalog-backend-module-msgraph/src/service/MicrosoftGraphOrgEntityProviderCatalogModule.ts @@ -15,11 +15,9 @@ */ import { - configServiceRef, + coreServices, createBackendModule, - loggerServiceRef, loggerToWinstonLogger, - schedulerServiceRef, } from '@backstage/backend-plugin-api'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node'; import { @@ -72,9 +70,9 @@ export const microsoftGraphOrgEntityProviderCatalogModule = createBackendModule( env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, - config: configServiceRef, - logger: loggerServiceRef, - scheduler: schedulerServiceRef, + config: coreServices.config, + logger: coreServices.logger, + scheduler: coreServices.scheduler, }, async init({ catalog, config, logger, scheduler }) { catalog.addEntityProvider( diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 0dc46b07a7..9251286f38 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.5-next.1 + +### Patch Changes + +- dd995cdc72: Enabled support of resolving `$refs` in all kind of yaml documents, not only OpenAPI. This implicitly adds `$ref` resolving support for AsyncAPI specs. Thus, the `openApiPlaceholderResolver` has been renamed to `jsonSchemaRefPlaceholderResolver`. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.1.4 + +### Patch Changes + +- 4ce887400d: Added support to use the `UrlReaders` when `$ref` pointing to a URL. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/config@1.0.4 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + ## 0.1.4-next.0 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/README.md b/plugins/catalog-backend-module-openapi/README.md index f80798e267..6b988e15f5 100644 --- a/plugins/catalog-backend-module-openapi/README.md +++ b/plugins/catalog-backend-module-openapi/README.md @@ -1,8 +1,10 @@ -# Catalog Backend Module for OpenAPI specifications +# Catalog Backend Module to resolve $refs in yaml documents -This is an extension module to the plugin-catalog-backend plugin, providing extensions targeted at OpenAPI specifications. +This is an extension module to the Catalog backend, providing extensions to resolve $refs in yaml documents. -With this you can split your OpenAPI definition into multiple files and reference them. They will be bundled, using an UrlReader, during processing and stored as a single specification. +With this you can split your yaml documents into multiple files and reference them. They will be bundled, using an UrlReader, during processing and stored as a single specification. + +This is useful for OpenAPI and AsyncAPI specifications. ## Installation @@ -15,15 +17,18 @@ yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-openapi ### Adding the plugin to your `packages/backend` -#### **openApiPlaceholderResolver** +#### **jsonSchemaRefPlaceholderResolver** -The placeholder resolver can be added by importing `openApiPlaceholderResolver` in `src/plugins/catalog.ts` in your `backend` package and adding the following. +The placeholder resolver can be added by importing `jsonSchemaRefPlaceholderResolver` in `src/plugins/catalog.ts` in your `backend` package and adding the following. ```ts -builder.setPlaceholderResolver('openapi', openApiPlaceholderResolver); +builder.setPlaceholderResolver('openapi', jsonSchemaRefPlaceholderResolver); +builder.setPlaceholderResolver('asyncapi', jsonSchemaRefPlaceholderResolver); ``` -This allows you to use the `$openapi` placeholder when referencing your OpenAPI specification. This will then resolve all `$ref` instances in your specification. +This allows you to use the `$openapi` placeholder when referencing your OpenAPI specification and `$asyncapi` when referencing your AsyncAPI specifications. This will then resolve all `$ref` instances in your specification. + +You can also use this resolver for other kind of yaml files to resolve $ref pointer. ```yaml apiVersion: backstage.io/v1alpha1 diff --git a/plugins/catalog-backend-module-openapi/api-report.md b/plugins/catalog-backend-module-openapi/api-report.md index d3a40067c5..c2af9cbe01 100644 --- a/plugins/catalog-backend-module-openapi/api-report.md +++ b/plugins/catalog-backend-module-openapi/api-report.md @@ -14,10 +14,13 @@ import { ScmIntegrations } from '@backstage/integration'; import { UrlReader } from '@backstage/backend-common'; // @public (undocumented) -export function openApiPlaceholderResolver( +export function jsonSchemaRefPlaceholderResolver( params: PlaceholderResolverParams, ): Promise; +// @public @deprecated (undocumented) +export const openApiPlaceholderResolver: typeof jsonSchemaRefPlaceholderResolver; + // @public @deprecated (undocumented) export class OpenApiRefProcessor implements CatalogProcessor { constructor(options: { diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index 6f21b8b2e0..3f7ae47930 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", - "version": "0.1.4-next.0", + "version": "0.1.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "start": "backstage-cli package start" }, "dependencies": { - "@apidevtools/swagger-parser": "^10.1.0", + "@apidevtools/json-schema-ref-parser": "^9.0.6", "@backstage/backend-common": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", diff --git a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts index 9f97e621c2..78568fd19f 100644 --- a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts +++ b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.test.ts @@ -17,13 +17,13 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { LocationSpec } from '@backstage/plugin-catalog-backend'; import { OpenApiRefProcessor } from './OpenApiRefProcessor'; -import { bundleOpenApiSpecification } from './lib'; +import { bundleFileWithRefs } from './lib'; jest.mock('./lib', () => ({ - bundleOpenApiSpecification: jest.fn(), + bundleFileWithRefs: jest.fn(), })); -const bundledSpecification = ''; +const bundled = ''; describe('OpenApiRefProcessor', () => { const mockLocation = (): LocationSpec => ({ @@ -32,7 +32,7 @@ describe('OpenApiRefProcessor', () => { }); beforeEach(() => { - (bundleOpenApiSpecification as any).mockResolvedValue(bundledSpecification); + (bundleFileWithRefs as any).mockResolvedValue(bundled); }); afterEach(() => { @@ -71,7 +71,7 @@ describe('OpenApiRefProcessor', () => { mockLocation(), ); - expect(result.spec?.definition).toEqual(bundledSpecification); + expect(result.spec?.definition).toEqual(bundled); }); it('should ignore other kinds', async () => { diff --git a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts index f4cffdf48f..6298aea77d 100644 --- a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts +++ b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts @@ -21,7 +21,7 @@ import { CatalogProcessor, LocationSpec, } from '@backstage/plugin-catalog-backend'; -import { bundleOpenApiSpecification } from './lib'; +import { bundleFileWithRefs } from './lib'; import { Logger } from 'winston'; /** @@ -84,7 +84,7 @@ export class OpenApiRefProcessor implements CatalogProcessor { this.logger.debug(`Bundling OpenAPI specification from ${location.target}`); try { - const bundledSpec = await bundleOpenApiSpecification( + const bundledSpec = await bundleFileWithRefs( definition.toString(), location.target, this.reader.read, diff --git a/plugins/catalog-backend-module-openapi/src/index.ts b/plugins/catalog-backend-module-openapi/src/index.ts index ccd3ced71e..170c925a7f 100644 --- a/plugins/catalog-backend-module-openapi/src/index.ts +++ b/plugins/catalog-backend-module-openapi/src/index.ts @@ -13,5 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { jsonSchemaRefPlaceholderResolver } from './jsonSchemaRefPlaceholderResolver'; + export { OpenApiRefProcessor } from './OpenApiRefProcessor'; -export { openApiPlaceholderResolver } from './openApiPlaceholderResolver'; +export { jsonSchemaRefPlaceholderResolver } from './jsonSchemaRefPlaceholderResolver'; +/** + * @public + * @deprecated replaced by jsonSchemaRefPlaceholderResolver + */ +export const openApiPlaceholderResolver = jsonSchemaRefPlaceholderResolver; diff --git a/plugins/catalog-backend-module-openapi/src/openApiPlaceholderResolver.test.ts b/plugins/catalog-backend-module-openapi/src/jsonSchemaRefPlaceholderResolver.test.ts similarity index 66% rename from plugins/catalog-backend-module-openapi/src/openApiPlaceholderResolver.test.ts rename to plugins/catalog-backend-module-openapi/src/jsonSchemaRefPlaceholderResolver.test.ts index f58a818ba5..afa523ccf0 100644 --- a/plugins/catalog-backend-module-openapi/src/openApiPlaceholderResolver.test.ts +++ b/plugins/catalog-backend-module-openapi/src/jsonSchemaRefPlaceholderResolver.test.ts @@ -14,16 +14,16 @@ * limitations under the License. */ import { PlaceholderResolverParams } from '@backstage/plugin-catalog-backend'; -import { openApiPlaceholderResolver } from './openApiPlaceholderResolver'; -import { bundleOpenApiSpecification } from './lib'; +import { jsonSchemaRefPlaceholderResolver } from './jsonSchemaRefPlaceholderResolver'; +import { bundleFileWithRefs } from './lib'; jest.mock('./lib', () => ({ - bundleOpenApiSpecification: jest.fn(), + bundleFileWithRefs: jest.fn(), })); -const bundledSpecification = ''; +const bundled = ''; -describe('openApiPlaceholderResolver', () => { +describe('jsonSchemaRefPlaceholderResolver', () => { const mockResolveUrl = jest.fn(); mockResolveUrl.mockReturnValue('mockUrl'); @@ -40,7 +40,7 @@ describe('openApiPlaceholderResolver', () => { }; beforeEach(() => { - (bundleOpenApiSpecification as any).mockResolvedValue(bundledSpecification); + (bundleFileWithRefs as any).mockResolvedValue(bundled); }); afterEach(() => { @@ -48,16 +48,14 @@ describe('openApiPlaceholderResolver', () => { }); it('should throw error if unable to bundle the OpenAPI specification', async () => { - (bundleOpenApiSpecification as any).mockRejectedValue(new Error('TEST')); + (bundleFileWithRefs as any).mockRejectedValue(new Error('TEST')); - await expect(openApiPlaceholderResolver(params)).rejects.toThrow( - 'Placeholder $openapi unable to bundle OpenAPI specification', - ); + await expect(jsonSchemaRefPlaceholderResolver(params)).rejects.toThrow(); }); it('should bundle the OpenAPI specification', async () => { - const result = await openApiPlaceholderResolver(params); + const result = await jsonSchemaRefPlaceholderResolver(params); - expect(result).toEqual(bundledSpecification); + expect(result).toEqual(bundled); }); }); diff --git a/plugins/catalog-backend-module-openapi/src/openApiPlaceholderResolver.ts b/plugins/catalog-backend-module-openapi/src/jsonSchemaRefPlaceholderResolver.ts similarity index 90% rename from plugins/catalog-backend-module-openapi/src/openApiPlaceholderResolver.ts rename to plugins/catalog-backend-module-openapi/src/jsonSchemaRefPlaceholderResolver.ts index 96fadb3cbc..befafbde8a 100644 --- a/plugins/catalog-backend-module-openapi/src/openApiPlaceholderResolver.ts +++ b/plugins/catalog-backend-module-openapi/src/jsonSchemaRefPlaceholderResolver.ts @@ -16,10 +16,10 @@ import { PlaceholderResolverParams } from '@backstage/plugin-catalog-backend'; import { JsonValue } from '@backstage/types'; import { processingResult } from '@backstage/plugin-catalog-node'; -import { bundleOpenApiSpecification } from './lib'; +import { bundleFileWithRefs } from './lib'; /** @public */ -export async function openApiPlaceholderResolver( +export async function jsonSchemaRefPlaceholderResolver( params: PlaceholderResolverParams, ): Promise { const { content, url } = await readTextLocation(params); @@ -27,7 +27,7 @@ export async function openApiPlaceholderResolver( params.emit(processingResult.refresh(`url:${url}`)); try { - return await bundleOpenApiSpecification( + return await bundleFileWithRefs( content, url, params.read, @@ -35,7 +35,7 @@ export async function openApiPlaceholderResolver( ); } catch (error) { throw new Error( - `Placeholder \$${params.key} unable to bundle OpenAPI specification at ${params.value}, ${error}`, + `Placeholder \$${params.key} unable to bundle the file at ${params.value}, ${error}`, ); } } diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts index 57ecd54f26..ec77610733 100644 --- a/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { bundleOpenApiSpecification } from './bundle'; +import { bundleFileWithRefs } from './bundle'; const specification = ` openapi: "3.0.0" @@ -70,7 +70,7 @@ paths: type: string `; -describe('bundleOpenApiSpecification', () => { +describe('bundleFileWithRefs', () => { const read = jest.fn(); const resolveUrl = jest.fn(); @@ -81,7 +81,7 @@ describe('bundleOpenApiSpecification', () => { it('should return the bundled specification', async () => { read.mockResolvedValue(list); - const result = await bundleOpenApiSpecification( + const result = await bundleFileWithRefs( specification, 'https://github.com/owner/repo/blob/main/catalog-info.yaml', read, @@ -108,7 +108,7 @@ describe('bundleOpenApiSpecification', () => { read.mockResolvedValue(list); - const result = await bundleOpenApiSpecification( + const result = await bundleFileWithRefs( spec, 'https://github.com/owner/repo/blob/main/catalog-info.yaml', read, @@ -117,4 +117,64 @@ describe('bundleOpenApiSpecification', () => { expect(result).toEqual(expectedResult.trimStart()); }); + it('should return the bundled asyncapi specification', async () => { + const spec = ` + asyncapi: 2.5.0 + info: + version: 1.0.0 + title: Sample API + description: A sample API to illustrate OpenAPI concepts + channels: + my-topic: + subscribe: + message: + schemaFormat: "application/schema+json;version=draft-07" + payload: + $ref : "./asyncapi.schema.json" + `; + const jsonSchema = ` + { + "type": "object", + "description": "ExampleSchema", + "properties": { + "name" : { + "type": "string" + }, + "age" : { + "type" : "integer" + } + } + } + `; + const expectedSchema = ` +asyncapi: 2.5.0 +info: + version: 1.0.0 + title: Sample API + description: A sample API to illustrate OpenAPI concepts +channels: + my-topic: + subscribe: + message: + schemaFormat: application/schema+json;version=draft-07 + payload: + type: object + description: ExampleSchema + properties: + name: + type: string + age: + type: integer +`; + read.mockResolvedValue(jsonSchema); + + const result = await bundleFileWithRefs( + spec, + 'https://github.com/owner/repo/blob/main/catalog-info.yaml', + read, + resolveUrl, + ); + + expect(result).toEqual(expectedSchema.trimStart()); + }); }); diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts index 8dd880c234..64c772b91e 100644 --- a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts +++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import SwaggerParser from '@apidevtools/swagger-parser'; +import $RefParser from '@apidevtools/json-schema-ref-parser'; import { parse, stringify } from 'yaml'; import * as path from 'path'; @@ -30,13 +30,13 @@ export type BundlerRead = (url: string) => Promise; export type BundlerResolveUrl = (url: string, base: string) => string; -export async function bundleOpenApiSpecification( - specification: string, +export async function bundleFileWithRefs( + fileWithRefs: string, baseUrl: string, read: BundlerRead, resolveUrl: BundlerResolveUrl, ): Promise { - const fileUrlReaderResolver: SwaggerParser.ResolverOptions = { + const fileUrlReaderResolver: $RefParser.ResolverOptions = { canRead: file => { const protocol = getProtocol(file.url); return protocol === undefined || protocol === 'file'; @@ -47,7 +47,7 @@ export async function bundleOpenApiSpecification( return await read(url); }, }; - const httpUrlReaderResolver: SwaggerParser.ResolverOptions = { + const httpUrlReaderResolver: $RefParser.ResolverOptions = { canRead: ref => { const protocol = getProtocol(ref.url); return protocol === 'http' || protocol === 'https'; @@ -58,13 +58,13 @@ export async function bundleOpenApiSpecification( }, }; - const options: SwaggerParser.Options = { + const options: $RefParser.Options = { resolve: { file: fileUrlReaderResolver, http: httpUrlReaderResolver, }, }; - const specObject = parse(specification); - const bundledSpec = await SwaggerParser.bundle(specObject, options); - return stringify(bundledSpec); + const fileObject = parse(fileWithRefs); + const bundledObject = await $RefParser.bundle(fileObject, options); + return stringify(bundledObject); } diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 494f332c12..601e61f06d 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,128 @@ # @backstage/plugin-catalog-backend +## 1.6.0-next.1 + +### Minor Changes + +- c395abb5b2: The catalog no longer stops after the first processor `validateEntityKind` + method returns `true` when validating entity kind shapes. Instead, it continues + through all registered processors that have this method, and requires that _at + least one_ of them returned true. + + The old behavior of stopping early made it harder to extend existing core kinds + with additional fields, since the `BuiltinKindsEntityProcessor` is always + present at the top of the processing chain and ensures that your additional + validation code would never be run. + + This is technically a breaking change, although it should not affect anybody + under normal circumstances, except if you had problematic validation code that + you were unaware that it was not being run. That code may now start to exhibit + those problems. + + If you need to disable this new behavior, `CatalogBuilder` as used in your + `packages/backend/src/plugins/catalog.ts` file now has a + `useLegacySingleProcessorValidation()` method to go back to the old behavior. + + ```diff + const builder = await CatalogBuilder.create(env); + +builder.useLegacySingleProcessorValidation(); + ``` + +### Patch Changes + +- 2a8e3cc0b5: Optimize `Stitcher` process to be more memory efficient +- 5b3e2afa45: Fixed deprecated use of `substr` into `substring`. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/plugin-permission-node@0.7.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-scaffolder-common@1.2.3-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.6.0-next.0 + +### Minor Changes + +- 16891a212c: Added new `POST /entities/by-refs` endpoint, which allows you to efficiently + batch-fetch entities by their entity ref. This can be useful e.g. in graphql + resolvers or similar contexts where you need to fetch many entities at the same + time. + +### Patch Changes + +- d8593ce0e6: Do not use deprecated `LocationSpec` from the `@backstage/plugin-catalog-node` package +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- e982f77fe3: Registered shutdown hook in experimental catalog plugin. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-permission-node@0.7.2-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-scaffolder-common@1.2.3-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 1.5.1 + +### Patch Changes + +- c1a4addda3: Improve processing error logging. + + Adds `location` and `owner` to the logging meta if they are available. + +- a7607b5413: Replace usage of deprecataed `UrlReader.read` with `UrlReader.readUrl`. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/plugin-permission-node@0.7.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-scaffolder-common@1.2.2 + - @backstage/plugin-search-common@1.1.1 + +## 1.5.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/plugin-permission-node@0.7.1-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-scaffolder-common@1.2.2-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 1.5.1-next.0 ### Patch Changes diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 10b9afc291..a46abda5d3 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -37,7 +37,7 @@ import { EntityRelationSpec } from '@backstage/plugin-catalog-node'; import { GetEntitiesRequest } from '@backstage/catalog-client'; import { JsonValue } from '@backstage/types'; import { LocationEntityV1alpha1 } from '@backstage/catalog-model'; -import { LocationSpec } from '@backstage/plugin-catalog-node'; +import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common'; import { Logger } from 'winston'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; @@ -86,9 +86,9 @@ export class AnnotateLocationEntityProcessor implements CatalogProcessor { // (undocumented) preProcessEntity( entity: Entity, - location: LocationSpec, + location: LocationSpec_2, _: CatalogProcessorEmit, - originLocation: LocationSpec, + originLocation: LocationSpec_2, ): Promise; } @@ -100,7 +100,7 @@ export class AnnotateScmSlugEntityProcessor implements CatalogProcessor { // (undocumented) getProcessorName(): string; // (undocumented) - preProcessEntity(entity: Entity, location: LocationSpec): Promise; + preProcessEntity(entity: Entity, location: LocationSpec_2): Promise; } // @public (undocumented) @@ -110,7 +110,7 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { // (undocumented) postProcessEntity( entity: Entity, - _location: LocationSpec, + _location: LocationSpec_2, emit: CatalogProcessorEmit, ): Promise; // (undocumented) @@ -133,7 +133,7 @@ export class CatalogBuilder { ...permissionRules: Array< CatalogPermissionRule | Array > - ): void; + ): this; addProcessor( ...processors: Array> ): CatalogBuilder; @@ -164,6 +164,7 @@ export class CatalogBuilder { errors: Error[]; }) => Promise | void; }): void; + useLegacySingleProcessorValidation(): this; } // @alpha @@ -284,7 +285,7 @@ export class CodeOwnersProcessor implements CatalogProcessor { // (undocumented) getProcessorName(): string; // (undocumented) - preProcessEntity(entity: Entity, location: LocationSpec): Promise; + preProcessEntity(entity: Entity, location: LocationSpec_2): Promise; } // @alpha @@ -409,7 +410,7 @@ export class FileReaderProcessor implements CatalogProcessor { getProcessorName(): string; // (undocumented) readLocation( - location: LocationSpec, + location: LocationSpec_2, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, @@ -431,7 +432,7 @@ export class LocationEntityProcessor implements CatalogProcessor { // (undocumented) postProcessEntity( entity: Entity, - location: LocationSpec, + location: LocationSpec_2, emit: CatalogProcessorEmit, ): Promise; } @@ -441,18 +442,19 @@ export type LocationEntityProcessorOptions = { integrations: ScmIntegrationRegistry; }; -export { LocationSpec }; +// @public @deprecated +export type LocationSpec = LocationSpec_2; // @public (undocumented) export function locationSpecToLocationEntity(opts: { - location: LocationSpec; + location: LocationSpec_2; parentEntity?: Entity; }): LocationEntityV1alpha1; // @public (undocumented) export function parseEntityYaml( data: Buffer, - location: LocationSpec, + location: LocationSpec_2, ): Iterable; // @alpha @@ -518,7 +520,7 @@ export class PlaceholderProcessor implements CatalogProcessor { // (undocumented) preProcessEntity( entity: Entity, - location: LocationSpec, + location: LocationSpec_2, emit: CatalogProcessorEmit, ): Promise; } @@ -574,7 +576,7 @@ export class UrlReaderProcessor implements CatalogProcessor { getProcessorName(): string; // (undocumented) readLocation( - location: LocationSpec, + location: LocationSpec_2, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 950ed2f63c..daa70656bc 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -25,7 +25,7 @@ export interface Config { * An undefined list of matchers means match all, an empty list of * matchers means match none. * - * This is commonly used to put in what amounts to a whitelist of kinds + * This is commonly used to put in what amounts to an allowlist of kinds * that regular users of Backstage are permitted to register locations * for. This can be used to stop them from registering yaml files * describing for example a Group entity called "admin" that they make diff --git a/plugins/catalog-backend/migrations/20200511113813_init.js b/plugins/catalog-backend/migrations/20200511113813_init.js index aa2ff6affa..710ff537e0 100644 --- a/plugins/catalog-backend/migrations/20200511113813_init.js +++ b/plugins/catalog-backend/migrations/20200511113813_init.js @@ -27,7 +27,7 @@ exports.up = async function up(knex) { // .createTable('locations', table => { table.comment( - 'Registered locations that shall be contiuously scanned for catalog item updates', + 'Registered locations that shall be continuously scanned for catalog item updates', ); table .uuid('id') @@ -59,7 +59,7 @@ exports.up = async function up(knex) { 'An opaque string that changes for each update operation to any part of the entity, including metadata.', ); table - .string('generation') + .integer('generation') .notNullable() .unsigned() .comment( diff --git a/plugins/catalog-backend/migrations/20200702153613_entities.js b/plugins/catalog-backend/migrations/20200702153613_entities.js index 86e0e48e40..37aee4d985 100644 --- a/plugins/catalog-backend/migrations/20200702153613_entities.js +++ b/plugins/catalog-backend/migrations/20200702153613_entities.js @@ -31,6 +31,7 @@ exports.up = async function up(knex) { } await knex.schema.alterTable('entities', table => { table.dropUnique([], 'entities_unique_name'); + table.dropForeign(['location_id']); }); // Setup temporary tables await knex.schema.renameTable('entities_search', 'tmp_entities_search'); @@ -56,7 +57,7 @@ exports.up = async function up(knex) { 'An opaque string that changes for each update operation to any part of the entity, including metadata.', ); table - .string('generation') + .integer('generation') .notNullable() .unsigned() .comment( diff --git a/plugins/catalog-backend/migrations/20200807120600_entitySearch.js b/plugins/catalog-backend/migrations/20200807120600_entitySearch.js index 9c7d966087..5acfbeb9ba 100644 --- a/plugins/catalog-backend/migrations/20200807120600_entitySearch.js +++ b/plugins/catalog-backend/migrations/20200807120600_entitySearch.js @@ -23,7 +23,7 @@ exports.up = async function up(knex) { // Sqlite does not support alter column. if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities_search', table => { - table.text('value').nullable().alter({ alterType: true }); + table.string('value').nullable().alter({ alterType: true }); }); } }; diff --git a/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js b/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js index 01be4789d0..9f0cabac41 100644 --- a/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js +++ b/plugins/catalog-backend/migrations/20200923104503_case_insensitivity.js @@ -24,8 +24,8 @@ exports.up = async function up(knex) { .where({ namespace: null }) .update({ namespace: 'default' }); await knex('entities_search').update({ - key: knex.raw('LOWER(key)'), - value: knex.raw('LOWER(value)'), + key: knex.raw('LOWER(??)', ['key']), + value: knex.raw('LOWER(??)', ['value']), }); }; diff --git a/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js index 941b8a0a3f..412791d8d7 100644 --- a/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js +++ b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js @@ -21,19 +21,20 @@ */ exports.up = async function up(knex) { await knex.schema.alterTable('entities', table => { - table.text('full_name').nullable(); + table.string('full_name').nullable(); }); await knex('entities').update({ full_name: knex.raw( - "LOWER(kind) || ':' || LOWER(COALESCE(namespace, 'default')) || '/' || LOWER(name)", + "LOWER(??) || ':' || LOWER(COALESCE(??, 'default')) || '/' || LOWER(??)", + ['kind', 'namespace', 'name'], ), }); // SQLite does not support alter column if (!knex.client.config.client.includes('sqlite3')) { await knex.schema.alterTable('entities', table => { - table.text('full_name').notNullable().alter({ alterNullable: true }); + table.string('full_name').notNullable().alter({ alterNullable: true }); }); } diff --git a/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js b/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js index 47097fc788..6e7ae9b8fe 100644 --- a/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js +++ b/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js @@ -31,7 +31,8 @@ exports.up = async function up(knex) { // apiVersion and kind should not contain any JSON unsafe chars, and both // metadata and spec are already valid serialized JSON data: knex.raw( - `'{"apiVersion":"' || api_version || '","kind":"' || kind || '","metadata":' || metadata || COALESCE(',"spec":' || spec, '') || '}'`, + `'{"apiVersion":"' || ?? || '","kind":"' || ?? || '","metadata":' || ?? || COALESCE(',"spec":' || ??, '') || '}'`, + ['api_version', 'kind', 'metadata', 'spec'], ), }); diff --git a/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js b/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js index 996c69de8c..313eb88600 100644 --- a/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js +++ b/plugins/catalog-backend/migrations/20201123205611_relations_table_uniq.js @@ -24,7 +24,7 @@ exports.up = async function up(knex) { // sqlite doesn't support dropPrimary so we recreate it properly instead await knex.schema.dropTable('entities_relations'); await knex.schema.createTable('entities_relations', table => { - table.comment('All relations between entities in the catalog'); + table.comment('All relations between entities'); table .uuid('originating_entity_id') .references('id') @@ -61,7 +61,7 @@ exports.down = async function down(knex) { if (knex.client.config.client.includes('sqlite3')) { await knex.schema.dropTable('entities_relations'); await knex.schema.createTable('entities_relations', table => { - table.comment('All relations between entities in the catalog'); + table.comment('All relations between entities'); table .uuid('originating_entity_id') .references('id') diff --git a/plugins/catalog-backend/migrations/20210302150147_refresh_state.js b/plugins/catalog-backend/migrations/20210302150147_refresh_state.js index da276940fa..1c8081c0ef 100644 --- a/plugins/catalog-backend/migrations/20210302150147_refresh_state.js +++ b/plugins/catalog-backend/migrations/20210302150147_refresh_state.js @@ -20,39 +20,30 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { + const isMySQL = knex.client.config.client.includes('mysql'); await knex.schema.createTable('refresh_state', table => { - table.comment( - 'Location refresh states. Every individual location (that was ever directly or indirectly discovered) and entity has an entry in this table. It therefore represents the entire live set of things that the refresh loop considers.', - ); + table.comment('Location refresh states'); table - .text('entity_id') + .string('entity_id') .primary() .notNullable() - .comment( - 'Primary ID, which will also be used as the uid of the resulting entity', - ); + .comment('Primary ID, also used as the uid of the entity'); table - .text('entity_ref') + .string('entity_ref') .notNullable() - .comment('A reference to the entity that the refresh state is tied to'); + .comment('A reference to the entity for this refresh state'); table .text('unprocessed_entity') .notNullable() - .comment( - 'The unprocessed entity (in its source form, before being run through all of the processors) as JSON', - ); + .comment('The unprocessed entity (in original form) as JSON'); table .text('processed_entity') .nullable() - .comment( - 'The processed entity (after running through all processors, but before being stitched together with state and relations) as JSON', - ); + .comment('The processed entity (not yet stitched) as JSON'); table .text('cache') .nullable() - .comment( - 'Cache information tied to the refreshing of this entity, such as etag information or actual response caching', - ); + .comment('Cache information tied to refreshes of this entity'); table .text('errors') .notNullable() @@ -64,7 +55,7 @@ exports.up = async function up(knex) { table .dateTime('last_discovery_at') // TODO: timezone or change to epoch-millis or similar .notNullable() - .comment('The last timestamp of which this entity was discovered'); + .comment('The last timestamp that this entity was discovered'); table.unique(['entity_ref'], { indexName: 'refresh_state_entity_ref_uniq', }); @@ -74,31 +65,23 @@ exports.up = async function up(knex) { }); await knex.schema.createTable('final_entities', table => { - table.comment( - 'This table contains the final entity result after processing and stitching', - ); + table.comment('Final entities after processing and stitching'); table - .text('entity_id') + .string('entity_id') .primary() .notNullable() .references('entity_id') .inTable('refresh_state') .onDelete('CASCADE') - .comment( - 'Entity ID which corresponds to the ID in the refresh_state table', - ); + .comment('Entity ID -> refresh_state table'); table - .text('hash') + .string('hash') .notNullable() - .comment( - 'Stable hash of the entity data, to be used for caching and avoiding redundant work', - ); + .comment('Stable hash of the entity data'); table .text('stitch_ticket') .notNullable() - .comment( - 'A random value representing a unique stitch attempt ticket, that gets updated each time that a stitching attempt is made on the entity', - ); + .comment('Random value representing a unique stitch attempt ticket'); table .text('final_entity') .nullable() @@ -107,29 +90,24 @@ exports.up = async function up(knex) { }); await knex.schema.createTable('refresh_state_references', table => { - table.comment( - 'Holds edges between refresh state rows. Every time when an entity is processed and emits another entity, an edge will be stored to represent that fact. This is used to detect orphans and ultimately deletions.', - ); + const textColumn = isMySQL + ? table.string.bind(table) + : table.text.bind(table); + + table.comment('Edges between refresh state rows'); table .increments('id') .comment('Primary key to distinguish unique lines from each other'); - table - .text('source_key') + textColumn('source_key') .nullable() - .comment( - 'When the reference source is not an entity, this is an opaque identifier for that source.', - ); - table - .text('source_entity_ref') + .comment('Opaque identifier for non-entity sources'); + textColumn('source_entity_ref') .nullable() .references('entity_ref') .inTable('refresh_state') .onDelete('CASCADE') - .comment( - 'When the reference source is an entity, this is the EntityRef of the source entity.', - ); - table - .text('target_entity_ref') + .comment('EntityRef of entity sources'); + textColumn('target_entity_ref') .notNullable() .references('entity_ref') .inTable('refresh_state') @@ -147,36 +125,34 @@ exports.up = async function up(knex) { }); await knex.schema.createTable('relations', table => { - table.comment('All relations between entities in the catalog'); + table.comment('All relations between entities'); table - .text('originating_entity_id') + .string('originating_entity_id') .references('entity_id') .inTable('refresh_state') .onDelete('CASCADE') .notNullable() .comment('The entity that provided the relation'); table - .text('source_entity_ref') + .string('source_entity_ref') .notNullable() - .comment('The entity reference of the source entity of the relation'); + .comment('Entity reference of the source entity of the relation'); table - .text('type') + .string('type') .notNullable() .comment('The type of the relation between the entities'); table - .text('target_entity_ref') + .string('target_entity_ref') .notNullable() - .comment('The entity reference of the target entity of the relation'); + .comment('Entity reference of the target entity of the relation'); table.index('source_entity_ref', 'relations_source_entity_ref_idx'); table.index('originating_entity_id', 'relations_source_entity_id_idx'); }); await knex.schema.createTable('search', table => { - table.comment( - 'Flattened key-values from the entities, used for quick filtering', - ); + table.comment('Flattened key-values from the entities, for filtering'); table - .text('entity_id') + .string('entity_id') .references('entity_id') .inTable('refresh_state') .onDelete('CASCADE') diff --git a/plugins/catalog-backend/migrations/20210622104022_refresh_state_location_key.js b/plugins/catalog-backend/migrations/20210622104022_refresh_state_location_key.js index 37ab5e12cb..f15e2c208c 100644 --- a/plugins/catalog-backend/migrations/20210622104022_refresh_state_location_key.js +++ b/plugins/catalog-backend/migrations/20210622104022_refresh_state_location_key.js @@ -24,9 +24,7 @@ exports.up = async function up(knex) { table .text('location_key') .nullable() - .comment( - 'An opaque key that uniquely identifies the location of an entity in order to support conflict resolution', - ); + .comment('Opaque conflict resolution key'); }); }; diff --git a/plugins/catalog-backend/migrations/20210925102509_add_refresh_state_input_hash.js b/plugins/catalog-backend/migrations/20210925102509_add_refresh_state_input_hash.js index 7d7a4ac578..136be9899e 100644 --- a/plugins/catalog-backend/migrations/20210925102509_add_refresh_state_input_hash.js +++ b/plugins/catalog-backend/migrations/20210925102509_add_refresh_state_input_hash.js @@ -24,7 +24,7 @@ exports.up = async function up(knex) { table .text('unprocessed_hash') .nullable() - .comment('A hash of the unprocessed contents, used to detect changes'); + .comment('A hash of the unprocessed contents'); }); }; diff --git a/plugins/catalog-backend/migrations/20220616202842_refresh_keys.js b/plugins/catalog-backend/migrations/20220616202842_refresh_keys.js index b1b67f68f2..a7ec819489 100644 --- a/plugins/catalog-backend/migrations/20220616202842_refresh_keys.js +++ b/plugins/catalog-backend/migrations/20220616202842_refresh_keys.js @@ -14,6 +14,8 @@ * limitations under the License. */ +// @ts-check + /** * @param { import("knex").Knex } knex */ @@ -23,14 +25,14 @@ exports.up = async function up(knex) { 'This table contains relations between entities and keys to trigger refreshes with', ); table - .text('entity_id') + .string('entity_id') .notNullable() .references('entity_id') .inTable('refresh_state') .onDelete('CASCADE') .comment('A reference to the entity that the refresh key is tied to'); table - .text('key') + .string('key') .notNullable() .comment( 'A reference to a key which should be used to trigger a refresh on this entity', diff --git a/plugins/catalog-backend/migrations/20221109192547_search_add_original_value_column.js b/plugins/catalog-backend/migrations/20221109192547_search_add_original_value_column.js new file mode 100644 index 0000000000..fff7955530 --- /dev/null +++ b/plugins/catalog-backend/migrations/20221109192547_search_add_original_value_column.js @@ -0,0 +1,51 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param { import("knex").Knex } knex + */ +exports.up = async function up(knex) { + // Start out with an original_value column that's equal to the value column + await knex.schema.alterTable('search', table => { + table + .string('original_value') + .nullable() + .comment('Holds the corresponding original case sensitive value'); + }); + await knex('search').update({ original_value: knex.ref('value') }); + + // Make sure to reprocess everything, to make sure that the original_value + // column is populated with values with the proper casing. It's unfortunately + // not enough to just reset the final_entities hash, since stitching is driven + // only by processing resulting in data that isn't matching the refresh_state + // hash. + await knex('refresh_state').update({ + result_hash: '', + next_update_at: knex.fn.now(), + }); + await knex('final_entities').update({ hash: '' }); +}; + +/** + * @param { import("knex").Knex } knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('search', table => { + table.dropColumn('original_value'); + }); +}; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index b18a133505..4d1b5fbefd 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "1.5.1-next.0", + "version": "1.6.0-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -80,7 +80,7 @@ "@types/uuid": "^8.0.0", "better-sqlite3": "^7.5.0", "luxon": "^3.0.0", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2" }, diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 34060814aa..84673a3900 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -80,6 +80,36 @@ export type EntitiesResponse = { pageInfo: PageInfo; }; +/** + * A request for a batch of entities. + */ +export interface EntitiesBatchRequest { + /** + * The refs for which to fetch entities. + */ + entityRefs: string[]; + /** + * Any additional filters to apply in the selection of the entities. + */ + filter?: EntityFilter; + /** + * Strips out only the parts of the entity bodies to include in the response. + */ + fields?: (entity: Entity) => Entity; + /** + * The optional token that authorizes the action. + */ + authorizationToken?: string; +} + +export interface EntitiesBatchResponse { + /** + * The list of entities, in the same order as the refs in the request. Entries + * that are null signify that no entity existed with that ref. + */ + items: Array; +} + export type EntityAncestryResponse = { rootEntityRef: string; items: Array<{ @@ -130,6 +160,11 @@ export interface EntitiesCatalog { */ entities(request?: EntitiesRequest): Promise; + /** + * Fetches a batch of entities. + */ + entitiesBatch(request: EntitiesBatchRequest): Promise; + /** * Removes a single entity. * diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts index 46b327733d..1056557706 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts @@ -36,7 +36,7 @@ import { generateStableHash } from './util'; describe('Default Processing Database', () => { const defaultLogger = getVoidLogger(); const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); async function createDatabase( @@ -991,13 +991,7 @@ describe('Default Processing Database', () => { added: [], removed: [ { - entity: { - apiVersion: '1.0.0', - metadata: { - name: 'new-root', - }, - kind: 'Location', - } as Entity, + entityRef: 'location:default/new-root', locationKey: 'file:/tmp/foobar', }, ], diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts index 3dc5325178..97259bce29 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts @@ -15,7 +15,7 @@ */ import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -import { ConflictError, isError, NotFoundError } from '@backstage/errors'; +import { ConflictError, NotFoundError } from '@backstage/errors'; import { Knex } from 'knex'; import lodash from 'lodash'; import { v4 as uuid } from 'uuid'; @@ -82,6 +82,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { refreshKeys, locationKey, } = options; + const configClient = tx.client.config.client; const refreshResult = await tx('refresh_state') .update({ processed_entity: JSON.stringify(processedEntity), @@ -114,10 +115,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { // Delete old relations // NOTE(freben): knex implemented support for returning() on update queries for sqlite, but at the current time of writing (Sep 2022) not for delete() queries. let previousRelationRows: DbRelationsRow[]; - if ( - tx.client.config.client.includes('sqlite3') || - tx.client.config.client.includes('mysql') - ) { + if (configClient.includes('sqlite3') || configClient.includes('mysql')) { previousRelationRows = await tx('relations') .select('*') .where({ originating_entity_id: id }); @@ -663,11 +661,11 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { last_discovery_at: tx.fn.now(), }); - // TODO(Rugvip): only tested towards Postgres and SQLite + // TODO(Rugvip): only tested towards MySQL, Postgres and SQLite. // We have to do this because the only way to detect if there was a conflict with // SQLite is to catch the error, while Postgres needs to ignore the conflict to not // break the ongoing transaction. - if (!tx.client.config.client.includes('sqlite3')) { + if (tx.client.config.client.includes('pg')) { query = query.onConflict('entity_ref').ignore() as any; // type here does not match runtime } @@ -675,14 +673,15 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { const result: { rowCount?: number; length?: number } = await query; return result.rowCount === 1 || result.length === 1; } catch (error) { - // SQLite reached this rather than the rowCount check above - if ( - isError(error) && - error.message.includes('UNIQUE constraint failed') - ) { + // SQLite, or MySQL reached this rather than the rowCount check above + if (!isDatabaseConflictError(error)) { + throw error; + } else { + this.options.logger.debug( + `Unable to insert a new refresh state row, ${error}`, + ); return false; } - throw error; } } @@ -737,7 +736,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { deferred: e, hash: generateStableHash(e.entity), })), - toRemove: options.removed.map(e => stringifyEntityRef(e.entity)), + toRemove: options.removed.map(e => e.entityRef), }; } diff --git a/plugins/catalog-backend/src/database/tables.ts b/plugins/catalog-backend/src/database/tables.ts index b9fe12be11..c23753b267 100644 --- a/plugins/catalog-backend/src/database/tables.ts +++ b/plugins/catalog-backend/src/database/tables.ts @@ -71,5 +71,6 @@ export type DbFinalEntitiesRow = { export type DbSearchRow = { entity_id: string; key: string; + original_value: string | null; value: string | null; }; diff --git a/plugins/catalog-backend/src/database/types.ts b/plugins/catalog-backend/src/database/types.ts index 2d30f8bd86..345aee6ebd 100644 --- a/plugins/catalog-backend/src/database/types.ts +++ b/plugins/catalog-backend/src/database/types.ts @@ -81,7 +81,7 @@ export type ReplaceUnprocessedEntitiesOptions = | { sourceKey: string; added: DeferredEntity[]; - removed: DeferredEntity[]; + removed: { entityRef: string; locationKey?: string }[]; type: 'delta'; }; diff --git a/plugins/catalog-backend/src/index.ts b/plugins/catalog-backend/src/index.ts index 88696498d0..000dd94e8b 100644 --- a/plugins/catalog-backend/src/index.ts +++ b/plugins/catalog-backend/src/index.ts @@ -22,7 +22,6 @@ export type { DeferredEntity, - LocationSpec, EntityRelationSpec, CatalogProcessor, CatalogProcessorParser, @@ -48,3 +47,19 @@ export * from './processing'; export * from './search'; export * from './service'; export * from './util'; + +import { LocationSpec as NonDeprecatedLocationSpec } from '@backstage/plugin-catalog-common'; + +/** + * Holds the entity location information. + * + * @remarks + * + * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch. + * This flag is then set to indicate that the file can be not present. + * default value: 'required'. + * + * @public + * @deprecated use the same type from `@backstage/plugin-catalog-common` instead + */ +export type LocationSpec = NonDeprecatedLocationSpec; diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index ce226bd09d..548c71d833 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -17,7 +17,7 @@ import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; import path from 'path'; -import { LocationSpec } from '@backstage/plugin-catalog-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; /** * Rules to apply to catalog entities. diff --git a/plugins/catalog-backend/src/integration.test.ts b/plugins/catalog-backend/src/integration.test.ts index dcfe9a1489..a63119bb67 100644 --- a/plugins/catalog-backend/src/integration.test.ts +++ b/plugins/catalog-backend/src/integration.test.ts @@ -245,6 +245,7 @@ class TestHarness { logger, parser: defaultEntityDataParser, policy: EntityPolicies.allOf([]), + legacySingleProcessorValidation: false, }); const stitcher = new Stitcher(db, logger); const catalog = new DefaultEntitiesCatalog(db, stitcher); diff --git a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts index 63f3d406f5..186b732271 100644 --- a/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/modules/codeowners/CodeOwnersProcessor.ts @@ -22,7 +22,8 @@ import { ScmIntegrations, } from '@backstage/integration'; import { Logger } from 'winston'; -import { CatalogProcessor, LocationSpec } from '@backstage/plugin-catalog-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; +import { CatalogProcessor } from '@backstage/plugin-catalog-node'; import { findCodeOwnerByTarget } from './lib'; const ALLOWED_KINDS = ['API', 'Component', 'Domain', 'Resource', 'System']; diff --git a/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts index a86525429e..d3329fa86c 100644 --- a/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateLocationEntityProcessor.ts @@ -25,10 +25,10 @@ import { } from '@backstage/catalog-model'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { identity, merge, pickBy } from 'lodash'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { CatalogProcessor, CatalogProcessorEmit, - LocationSpec, } from '@backstage/plugin-catalog-node'; /** @public */ diff --git a/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts index 41e75e5e8e..2c18ba1d7d 100644 --- a/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/AnnotateScmSlugEntityProcessor.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -21,7 +22,8 @@ import { } from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; import { identity, merge, pickBy } from 'lodash'; -import { CatalogProcessor, LocationSpec } from '@backstage/plugin-catalog-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; +import { CatalogProcessor } from '@backstage/plugin-catalog-node'; const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; const GITLAB_ACTIONS_ANNOTATION = 'gitlab.com/project-slug'; diff --git a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts index 6e05daf591..4dbb23addc 100644 --- a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts @@ -48,10 +48,10 @@ import { UserEntity, userEntityV1alpha1Validator, } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { CatalogProcessor, CatalogProcessorEmit, - LocationSpec, processingResult, } from '@backstage/plugin-catalog-node'; diff --git a/plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts index 2a9b1f8d65..9a957f8838 100644 --- a/plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts +++ b/plugins/catalog-backend/src/modules/core/DefaultLocationStore.test.ts @@ -20,7 +20,7 @@ import { DefaultLocationStore } from './DefaultLocationStore'; describe('DefaultLocationStore', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); async function createLocationStore(databaseId: TestDatabaseId) { diff --git a/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts index af6fba52c0..e0cd11fa3b 100644 --- a/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/FileReaderProcessor.ts @@ -18,11 +18,11 @@ import fs from 'fs-extra'; import g from 'glob'; import path from 'path'; import { promisify } from 'util'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, - LocationSpec, processingResult, } from '@backstage/plugin-catalog-node'; diff --git a/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts index 3676b4f727..1db74b3b56 100644 --- a/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.ts @@ -17,11 +17,11 @@ import { Entity, LocationEntity } from '@backstage/catalog-model'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { processingResult, CatalogProcessor, CatalogProcessorEmit, - LocationSpec, } from '@backstage/plugin-catalog-node'; export function toAbsoluteUrl( diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts index e6b5ee279d..79fcee960c 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.ts @@ -19,10 +19,10 @@ import { Entity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import yaml from 'yaml'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { CatalogProcessor, CatalogProcessorEmit, - LocationSpec, processingResult, } from '@backstage/plugin-catalog-node'; @@ -107,7 +107,7 @@ export class PlaceholderProcessor implements CatalogProcessor { return [data, false]; } - const resolverKey = keys[0].substr(1); + const resolverKey = keys[0].substring(1); const resolverValue = data[keys[0]]; const resolver = this.options.resolvers[resolverKey]; diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts index fe9f21ebb4..5e31dcbd1a 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.ts @@ -20,6 +20,7 @@ import { assertError } from '@backstage/errors'; import parseGitUrl from 'git-url-parse'; import limiterFactory from 'p-limit'; import { Logger } from 'winston'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { CatalogProcessor, CatalogProcessorCache, @@ -27,7 +28,6 @@ import { CatalogProcessorEntityResult, CatalogProcessorParser, CatalogProcessorResult, - LocationSpec, processingResult, } from '@backstage/plugin-catalog-node'; diff --git a/plugins/catalog-backend/src/modules/util/parse.ts b/plugins/catalog-backend/src/modules/util/parse.ts index d3aa915e49..778c176323 100644 --- a/plugins/catalog-backend/src/modules/util/parse.ts +++ b/plugins/catalog-backend/src/modules/util/parse.ts @@ -17,10 +17,10 @@ import { Entity, stringifyLocationRef } from '@backstage/catalog-model'; import lodash from 'lodash'; import yaml from 'yaml'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { CatalogProcessorParser, CatalogProcessorResult, - LocationSpec, processingResult, } from '@backstage/plugin-catalog-node'; diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts index 2cc0e2dc40..bef2878099 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { + ANNOTATION_LOCATION, + Entity, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { assertError, serializeError, stringifyError } from '@backstage/errors'; import { Hash } from 'crypto'; import stableStringify from 'fast-json-stable-stringify'; @@ -116,11 +120,12 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { }); } + const location = + unprocessedEntity?.metadata?.annotations?.[ANNOTATION_LOCATION]; for (const error of result.errors) { - // TODO(freben): Try to extract the location out of the unprocessed - // entity and add as meta to the log lines this.logger.warn(error.message, { entity: entityRef, + location, }); } const errorsString = JSON.stringify( diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts index cbb130d138..9731022582 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts @@ -97,6 +97,7 @@ describe('DefaultCatalogProcessingOrchestrator', () => { parser: defaultEntityDataParser, policy: EntityPolicies.allOf([]), rulesEnforcer: { isAllowed: () => true }, + legacySingleProcessorValidation: false, }); it('runs a minimal processing', async () => { @@ -190,6 +191,54 @@ describe('DefaultCatalogProcessingOrchestrator', () => { ok: true, }); }); + + it('runs all processor validations when asked to', async () => { + const validate = jest.fn(async () => true); + const processor1: Partial = { + validateEntityKind: validate, + }; + const processor2: Partial = { + validateEntityKind: validate, + }; + + const legacy = new DefaultCatalogProcessingOrchestrator({ + processors: [ + processor1 as CatalogProcessor, + processor2 as CatalogProcessor, + ], + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + logger: getVoidLogger(), + parser: defaultEntityDataParser, + policy: EntityPolicies.allOf([]), + rulesEnforcer: { isAllowed: () => true }, + legacySingleProcessorValidation: true, + }); + + const modern = new DefaultCatalogProcessingOrchestrator({ + processors: [ + processor1 as CatalogProcessor, + processor2 as CatalogProcessor, + ], + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + logger: getVoidLogger(), + parser: defaultEntityDataParser, + policy: EntityPolicies.allOf([]), + rulesEnforcer: { isAllowed: () => true }, + legacySingleProcessorValidation: false, + }); + + await expect(legacy.process({ entity })).resolves.toMatchObject({ + ok: true, + }); + expect(validate).toHaveBeenCalledTimes(1); + + validate.mockClear(); + + await expect(modern.process({ entity })).resolves.toMatchObject({ + ok: true, + }); + expect(validate).toHaveBeenCalledTimes(2); + }); }); describe('rules', () => { @@ -231,6 +280,7 @@ describe('DefaultCatalogProcessingOrchestrator', () => { parser, policy: EntityPolicies.allOf([]), rulesEnforcer, + legacySingleProcessorValidation: false, }); rulesEnforcer.isAllowed.mockReturnValueOnce(true); @@ -272,6 +322,7 @@ describe('DefaultCatalogProcessingOrchestrator', () => { parser, policy: EntityPolicies.allOf([new FailingEntityPolicy()]), rulesEnforcer, + legacySingleProcessorValidation: false, }); await expect( diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 79876f70d8..339469ff1d 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -32,10 +32,10 @@ import { JsonValue } from '@backstage/types'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; import { Logger } from 'winston'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { CatalogProcessor, CatalogProcessorParser, - LocationSpec, processingResult, } from '@backstage/plugin-catalog-node'; import { @@ -76,6 +76,7 @@ export class DefaultCatalogProcessingOrchestrator parser: CatalogProcessorParser; policy: EntityPolicy; rulesEnforcer: CatalogRulesEnforcer; + legacySingleProcessorValidation: boolean; }, ) {} @@ -253,19 +254,17 @@ export class DefaultCatalogProcessingOrchestrator ); } - let foundKind = false; + let valid = false; for (const processor of this.options.processors) { if (processor.validateEntityKind) { try { - foundKind = await processor.validateEntityKind(entity); - if (foundKind) { - // TODO(freben): It would make sense to keep running, so that - // multiple processors could have a go at making checks. For - // example, an org may want to add additional rules on top of the - // provided ones. But that would be a breaking change, so we'll - // postpone that to a future processors rewrite. - break; + const thisValid = await processor.validateEntityKind(entity); + if (thisValid) { + valid = true; + if (this.options.legacySingleProcessorValidation) { + break; + } } } catch (e) { throw new InputError( @@ -276,7 +275,7 @@ export class DefaultCatalogProcessingOrchestrator } } - if (!foundKind) { + if (!valid) { throw new InputError( `No processor recognized the entity ${context.entityRef} as valid, possibly caused by a foreign kind or apiVersion`, ); diff --git a/plugins/catalog-backend/src/processing/connectEntityProviders.ts b/plugins/catalog-backend/src/processing/connectEntityProviders.ts index a4795532f0..3eb638ce7a 100644 --- a/plugins/catalog-backend/src/processing/connectEntityProviders.ts +++ b/plugins/catalog-backend/src/processing/connectEntityProviders.ts @@ -17,6 +17,7 @@ import { Entity, entityEnvelopeSchemaValidator, + stringifyEntityRef, } from '@backstage/catalog-model'; import { ProcessingDatabase } from '../database/types'; import { @@ -50,13 +51,24 @@ class Connection implements EntityProviderConnection { }); } else if (mutation.type === 'delta') { this.check(mutation.added.map(e => e.entity)); - this.check(mutation.removed.map(e => e.entity)); + this.check( + mutation.removed + .map(e => ('entity' in e ? e.entity : undefined)) + .filter((e): e is Entity => Boolean(e)), + ); await db.transaction(async tx => { await db.replaceUnprocessedEntities(tx, { sourceKey: this.config.id, type: 'delta', added: mutation.added, - removed: mutation.removed, + removed: mutation.removed.map(r => + 'entityRef' in r + ? r + : { + entityRef: stringifyEntityRef(r.entity), + locationKey: r.locationKey, + }, + ), }); }); } diff --git a/plugins/catalog-backend/src/processing/util.ts b/plugins/catalog-backend/src/processing/util.ts index bd96763a54..cd93be3c8b 100644 --- a/plugins/catalog-backend/src/processing/util.ts +++ b/plugins/catalog-backend/src/processing/util.ts @@ -27,7 +27,7 @@ import { JsonObject, JsonValue } from '@backstage/types'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; -import { LocationSpec } from '@backstage/plugin-catalog-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; export function isLocationEntity(entity: Entity): entity is LocationEntity { return entity.kind === 'Location'; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts index 5fc8ca6fbf..8b4e5601b4 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts @@ -114,14 +114,14 @@ describe('DefaultCatalogCollatorFactory', () => { }); it('fetches from the configured catalog service', async () => { - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('catalog'); expect(documents).toHaveLength(expectedEntities.length); }); it('maps a returned entity to an expected CatalogEntityDocument', async () => { - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); expect(documents[0]).toMatchObject({ @@ -159,7 +159,7 @@ describe('DefaultCatalogCollatorFactory', () => { }); collator = await factory.getCollator(); - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); expect(documents[0]).toMatchObject({ location: '/software/test-entity', @@ -177,7 +177,7 @@ describe('DefaultCatalogCollatorFactory', () => { }); collator = await factory.getCollator(); - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); // The simulated 'Foo,Bar' filter should return in an empty list @@ -191,7 +191,7 @@ describe('DefaultCatalogCollatorFactory', () => { }); collator = await factory.getCollator(); - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); expect(documents).toHaveLength(expectedEntities.length); diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts index 66764dc89f..cbaf876071 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.test.ts @@ -24,6 +24,7 @@ import { AuthorizedEntitiesCatalog } from './AuthorizedEntitiesCatalog'; describe('AuthorizedEntitiesCatalog', () => { const fakeCatalog = { entities: jest.fn(), + entitiesBatch: jest.fn(), removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), facets: jest.fn(), @@ -92,6 +93,67 @@ describe('AuthorizedEntitiesCatalog', () => { }); }); + describe('entitiesBatch', () => { + it('returns empty response on DENY', async () => { + fakePermissionApi.authorizeConditional.mockResolvedValue([ + { result: AuthorizeResult.DENY }, + ]); + const catalog = createCatalog(); + + await expect( + catalog.entitiesBatch({ + entityRefs: ['component:default/component-a'], + authorizationToken: 'abcd', + }), + ).resolves.toEqual({ + items: [null], + }); + + expect(fakeCatalog.entitiesBatch).not.toHaveBeenCalled(); + }); + + it('calls underlying catalog method with correct filter on CONDITIONAL', async () => { + fakePermissionApi.authorizeConditional.mockResolvedValue([ + { + result: AuthorizeResult.CONDITIONAL, + conditions: { + rule: 'IS_ENTITY_KIND', + params: { kinds: ['b'] }, + }, + }, + ]); + const catalog = createCatalog(isEntityKind); + + await catalog.entitiesBatch({ + entityRefs: ['component:default/component-a'], + authorizationToken: 'abcd', + }); + + expect(fakeCatalog.entitiesBatch).toHaveBeenCalledWith({ + entityRefs: ['component:default/component-a'], + authorizationToken: 'abcd', + filter: { key: 'kind', values: ['b'] }, + }); + }); + + it('calls underlying catalog method on ALLOW', async () => { + fakePermissionApi.authorizeConditional.mockResolvedValue([ + { result: AuthorizeResult.ALLOW }, + ]); + const catalog = createCatalog(); + + await catalog.entitiesBatch({ + entityRefs: ['component:default/component-a'], + authorizationToken: 'abcd', + }); + + expect(fakeCatalog.entitiesBatch).toHaveBeenCalledWith({ + entityRefs: ['component:default/component-a'], + authorizationToken: 'abcd', + }); + }); + }); + describe('removeEntityByUid', () => { it('throws error on DENY', async () => { fakeCatalog.entities.mockResolvedValue({ diff --git a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts index 0e861a39e1..c62e9f9a84 100644 --- a/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts @@ -26,6 +26,8 @@ import { } from '@backstage/plugin-permission-common'; import { ConditionTransformer } from '@backstage/plugin-permission-node'; import { + EntitiesBatchRequest, + EntitiesBatchResponse, EntitiesCatalog, EntitiesRequest, EntitiesResponse, @@ -73,6 +75,37 @@ export class AuthorizedEntitiesCatalog implements EntitiesCatalog { return this.entitiesCatalog.entities(request); } + async entitiesBatch( + request: EntitiesBatchRequest, + ): Promise { + const authorizeDecision = ( + await this.permissionApi.authorizeConditional( + [{ permission: catalogEntityReadPermission }], + { token: request?.authorizationToken }, + ) + )[0]; + + if (authorizeDecision.result === AuthorizeResult.DENY) { + return { + items: new Array(request.entityRefs.length).fill(null), + }; + } + + if (authorizeDecision.result === AuthorizeResult.CONDITIONAL) { + const permissionFilter: EntityFilter = this.transformConditions( + authorizeDecision.conditions, + ); + return this.entitiesCatalog.entitiesBatch({ + ...request, + filter: request?.filter + ? { allOf: [permissionFilter, request.filter] } + : permissionFilter, + }); + } + + return this.entitiesCatalog.entitiesBatch(request); + } + async removeEntityByUid( uid: string, options?: { authorizationToken?: string }, diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index d5bc7057dd..8aab74c4ba 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -154,6 +154,7 @@ export class CatalogBuilder { private locationAnalyzer: LocationAnalyzer | undefined = undefined; private readonly permissionRules: CatalogPermissionRule[]; private allowedLocationType: string[]; + private legacySingleProcessorValidation = false; /** * Creates a catalog builder. @@ -384,6 +385,7 @@ export class CatalogBuilder { > ) { this.permissionRules.push(...permissionRules.flat()); + return this; } /** @@ -396,6 +398,15 @@ export class CatalogBuilder { return this; } + /** + * Enables the legacy behaviour of canceling validation early whenever only a + * single processor declares an entity kind to be valid. + */ + useLegacySingleProcessorValidation(): this { + this.legacySingleProcessorValidation = true; + return this; + } + /** * Wires up and returns all of the component parts of the catalog */ @@ -429,6 +440,7 @@ export class CatalogBuilder { logger, parser, policy, + legacySingleProcessorValidation: this.legacySingleProcessorValidation, }); const stitcher = new Stitcher(dbClient, logger); const unauthorizedEntitiesCatalog = new DefaultEntitiesCatalog( diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index 64e1e52494..6db54ed4fa 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -14,14 +14,9 @@ * limitations under the License. */ import { - configServiceRef, createBackendPlugin, - databaseServiceRef, - loggerServiceRef, + coreServices, loggerToWinstonLogger, - permissionsServiceRef, - urlReaderServiceRef, - httpRouterServiceRef, } from '@backstage/backend-plugin-api'; import { CatalogBuilder } from './CatalogBuilder'; import { @@ -72,12 +67,13 @@ export const catalogPlugin = createBackendPlugin({ env.registerInit({ deps: { - logger: loggerServiceRef, - config: configServiceRef, - reader: urlReaderServiceRef, - permissions: permissionsServiceRef, - database: databaseServiceRef, - httpRouter: httpRouterServiceRef, + logger: coreServices.logger, + config: coreServices.config, + reader: coreServices.urlReader, + permissions: coreServices.permissions, + database: coreServices.database, + httpRouter: coreServices.httpRouter, + lifecycle: coreServices.lifecycle, }, async init({ logger, @@ -86,6 +82,7 @@ export const catalogPlugin = createBackendPlugin({ database, permissions, httpRouter, + lifecycle, }) { const winstonLogger = loggerToWinstonLogger(logger); const builder = await CatalogBuilder.create({ @@ -100,7 +97,11 @@ export const catalogPlugin = createBackendPlugin({ const { processingEngine, router } = await builder.build(); await processingEngine.start(); - + lifecycle.addShutdownHook({ + fn: async () => { + await processingEngine.stop(); + }, + }); httpRouter.use(router); }, }); diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 49d4fe9e19..b84596c78c 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -26,11 +26,12 @@ import { DbSearchRow, } from '../database/tables'; import { Stitcher } from '../stitching/Stitcher'; +import { buildEntitySearch } from '../stitching/buildEntitySearch'; import { DefaultEntitiesCatalog } from './DefaultEntitiesCatalog'; describe('DefaultEntitiesCatalog', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); const stitch = jest.fn(); const stitcher: Stitcher = { stitch } as any; @@ -100,29 +101,14 @@ describe('DefaultEntitiesCatalog', () => { stitch_ticket: '', }); - await insertSearchRow(knex, id, null, entity); - } - - async function insertSearchRow( - knex: Knex, - id: string, - previousKey: string | null, - previousValue: Object, - ) { - return Promise.all( - Object.entries(previousValue).map(async ([key, value]) => { - const currentKey = `${previousKey ? `${previousKey}.` : ``}${key}`; - if (typeof value === 'object') { - await insertSearchRow(knex, id, currentKey, value); - } else { - await knex('search').insert({ - entity_id: id, - key: currentKey, - value: value, - }); - } - }), - ); + for (const row of buildEntitySearch(id, entity)) { + await knex('search').insert({ + entity_id: id, + key: row.key, + value: row.value, + original_value: row.original_value, + }); + } } afterEach(() => { @@ -534,6 +520,60 @@ describe('DefaultEntitiesCatalog', () => { ); }); + describe('entitiesBatch', () => { + it.each(databases.eachSupportedId())( + 'queries for entities by ref, including duplicates, and gracefully returns null for missing entities', + async databaseId => { + const { knex } = await createDatabase(databaseId); + + await addEntity( + knex, + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'one' }, + spec: {}, + relations: [], + }, + [], + ); + await addEntity( + knex, + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'two' }, + spec: {}, + relations: [], + }, + [], + ); + + const catalog = new DefaultEntitiesCatalog(knex, stitcher); + + const { items } = await catalog.entitiesBatch({ + entityRefs: [ + 'k:default/two', + 'k:default/one', + 'k:default/two', + 'not-even-a-ref', + 'k:default/does-not-exist', + 'k:default/two', + ], + }); + + expect(items.map(e => e && stringifyEntityRef(e))).toEqual([ + 'k:default/two', + 'k:default/one', + 'k:default/two', + null, + null, + 'k:default/two', + ]); + }, + ); + }); + describe('removeEntityByUid', () => { it.each(databases.eachSupportedId())( 'also clears parent hashes', @@ -738,8 +778,8 @@ describe('DefaultEntitiesCatalog', () => { facets: { 'metadata.tags': expect.arrayContaining([ { value: 'java', count: 2 }, - { value: 'rust', count: 1 }, { value: 'node', count: 1 }, + { value: 'rust', count: 1 }, ]), }, }); diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index dc84b12484..14f22f862b 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -20,9 +20,11 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { InputError, NotFoundError } from '@backstage/errors'; -import { Knex } from 'knex'; import lodash from 'lodash'; +import { Knex } from 'knex'; import { + EntitiesBatchRequest, + EntitiesBatchResponse, EntitiesCatalog, EntitiesRequest, EntitiesResponse, @@ -89,12 +91,13 @@ function addCondition( db: Knex, filter: EntitiesSearchFilter, negate: boolean = false, + entityIdField = 'entity_id', ) { // NOTE(freben): This used to be a set of OUTER JOIN, which may seem to // make a lot of sense. However, it had abysmal performance on sqlite // when datasets grew large, so we're using IN instead. const matchQuery = db('search') - .select('entity_id') + .select(entityIdField) .where({ key: filter.key.toLowerCase() }) .andWhere(function keyFilter() { if (filter.values) { @@ -109,7 +112,7 @@ function addCondition( } } }); - queryBuilder.andWhere('entity_id', negate ? 'not in' : 'in', matchQuery); + queryBuilder.andWhere(entityIdField, negate ? 'not in' : 'in', matchQuery); } function isEntitiesSearchFilter( @@ -135,25 +138,30 @@ function parseFilter( query: Knex.QueryBuilder, db: Knex, negate: boolean = false, + entityIdField = 'entity_id', ): Knex.QueryBuilder { if (isEntitiesSearchFilter(filter)) { return query.andWhere(function filterFunction() { - addCondition(this, db, filter, negate); + addCondition(this, db, filter, negate, entityIdField); }); } if (isNegationEntityFilter(filter)) { - return parseFilter(filter.not, query, db, !negate); + return parseFilter(filter.not, query, db, !negate, entityIdField); } return query[negate ? 'andWhereNot' : 'andWhere'](function filterFunction() { if (isOrEntityFilter(filter)) { for (const subFilter of filter.anyOf ?? []) { - this.orWhere(subQuery => parseFilter(subFilter, subQuery, db)); + this.orWhere(subQuery => + parseFilter(subFilter, subQuery, db, false, entityIdField), + ); } } else { for (const subFilter of filter.allOf ?? []) { - this.andWhere(subQuery => parseFilter(subFilter, subQuery, db)); + this.andWhere(subQuery => + parseFilter(subFilter, subQuery, db, false, entityIdField), + ); } } }); @@ -188,7 +196,6 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { } let rows = await entitiesQuery; - let pageInfo: DbPageInfo; if (limit === undefined || rows.length <= limit) { pageInfo = { hasNextPage: false }; @@ -237,7 +244,41 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { }; } + async entitiesBatch( + request: EntitiesBatchRequest, + ): Promise { + const lookup = new Map(); + + for (const chunk of lodash.chunk(request.entityRefs, 200)) { + let query = this.database('final_entities') + .innerJoin('refresh_state', { + 'refresh_state.entity_id': 'final_entities.entity_id', + }) + .select({ + entityRef: 'refresh_state.entity_ref', + entity: 'final_entities.final_entity', + }) + .whereIn('refresh_state.entity_ref', chunk); + if (request?.filter) { + query = parseFilter(request.filter, query, this.database); + } + for (const row of await query) { + lookup.set(row.entityRef, row.entity ? JSON.parse(row.entity) : null); + } + } + + let items = request.entityRefs.map(ref => lookup.get(ref) ?? null); + + if (request.fields) { + items = items.map(e => e && request.fields!(e)); + } + + return { items }; + } + async removeEntityByUid(uid: string): Promise { + const dbConfig = this.database.client.config; + // Clear the hashed state of the immediate parents of the deleted entity. // This makes sure that when they get reprocessed, their output is written // down again. The reason for wanting to do this, is that if the user @@ -246,21 +287,53 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { // means it'll never try to write down the children again (it assumes that // they already exist). This means that without the code below, the database // never "heals" from accidental deletes. - await this.database('refresh_state') - .update({ - result_hash: 'child-was-deleted', - next_update_at: this.database.fn.now(), - }) - .whereIn('entity_ref', function parents(builder) { - return builder - .from('refresh_state') - .innerJoin('refresh_state_references', { - 'refresh_state_references.target_entity_ref': - 'refresh_state.entity_ref', - }) - .where('refresh_state.entity_id', '=', uid) - .select('refresh_state_references.source_entity_ref'); - }); + if (dbConfig.client.includes('mysql')) { + // MySQL doesn't support the syntax we need to do this in a single query, + // http://dev.mysql.com/doc/refman/5.6/en/update.html + const results = await this.database('refresh_state') + .select('entity_id') + .whereIn('entity_ref', function parents(builder) { + return builder + .from('refresh_state') + .innerJoin( + 'refresh_state_references', + { + 'refresh_state_references.target_entity_ref': + 'refresh_state.entity_ref', + }, + ) + .where('refresh_state.entity_id', '=', uid) + .select('refresh_state_references.source_entity_ref'); + }); + await this.database('refresh_state') + .update({ + result_hash: 'child-was-deleted', + next_update_at: this.database.fn.now(), + }) + .whereIn( + 'entity_id', + results.map(key => key.entity_id), + ); + } else { + await this.database('refresh_state') + .update({ + result_hash: 'child-was-deleted', + next_update_at: this.database.fn.now(), + }) + .whereIn('entity_ref', function parents(builder) { + return builder + .from('refresh_state') + .innerJoin( + 'refresh_state_references', + { + 'refresh_state_references.target_entity_ref': + 'refresh_state.entity_ref', + }, + ) + .where('refresh_state.entity_id', '=', uid) + .select('refresh_state_references.source_entity_ref'); + }); + } // Stitch the entities that the deleted one had relations to. If we do not // do this, the entities in the other end of the relations will still look @@ -285,7 +358,6 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { .select({ ref: 'relations.source_entity_ref' }), ); - // Perform the actual deletion await this.database('refresh_state') .where('entity_id', uid) .delete(); @@ -358,44 +430,26 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { } async facets(request: EntityFacetsRequest): Promise { - const { entities } = await this.entities({ - filter: request.filter, - authorizationToken: request.authorizationToken, - }); - const facets: EntityFacetsResponse['facets'] = {}; + const db = this.database; for (const facet of request.facets) { - const values = entities - .map(entity => { - // TODO(freben): Generalize this code to handle any field that may - // have dots in its key? - if (facet.startsWith('metadata.annotations.')) { - return entity.metadata.annotations?.[ - facet.substring('metadata.annotations.'.length) - ]; - } else if (facet.startsWith('metadata.labels.')) { - return entity.metadata.labels?.[ - facet.substring('metadata.labels.'.length) - ]; - } - return lodash.get(entity, facet); - }) - .flatMap(field => { - if (typeof field === 'string') { - return [field]; - } else if (Array.isArray(field)) { - return field.filter(i => typeof i === 'string'); - } - return []; - }) - .sort(); + const dbQuery = db('search') + .join('final_entities', 'search.entity_id', 'final_entities.entity_id') + .where('search.key', facet.toLocaleLowerCase('en-US')) + .count('search.entity_id as count') + .select({ value: 'search.original_value' }) + .groupBy('search.original_value'); - const counts = lodash.countBy(values, lodash.identity); + if (request?.filter) { + parseFilter(request.filter, dbQuery, db, false, 'search.entity_id'); + } - facets[facet] = Object.entries(counts).map(([value, count]) => ({ - value, - count, + const result = await dbQuery; + + facets[facet] = result.map(data => ({ + value: String(data.value), + count: Number(data.count), })); } diff --git a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts index 419aedc15d..3ed4f3fe36 100644 --- a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts @@ -36,7 +36,7 @@ import { DefaultRefreshService } from './DefaultRefreshService'; describe('Refresh integration', () => { const defaultLogger = getVoidLogger(); const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); async function createDatabase( diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index f383df7364..541359feb6 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -48,6 +48,7 @@ describe('createRouter readonly disabled', () => { beforeAll(async () => { entitiesCatalog = { entities: jest.fn(), + entitiesBatch: jest.fn(), removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), facets: jest.fn(), @@ -257,6 +258,38 @@ describe('createRouter readonly disabled', () => { }); }); + describe('POST /entities/by-refs', () => { + it.each([ + '', + 'not json', + '[', + '[]', + '{}', + '{"unknown":7}', + '{"entityRefs":7}', + '{"entityRefs":[7]}', + ])('properly rejects malformed request body, %p', async p => { + await expect( + request(app) + .post('/entities/by-refs') + .set('Content-Type', 'application/json') + .send(p), + ).resolves.toMatchObject({ status: 400 }); + }); + + it('can fetch entities by refs', async () => { + const entity: Entity = {} as any; + entitiesCatalog.entitiesBatch.mockResolvedValue({ items: [entity] }); + const response = await request(app) + .post('/entities/by-refs') + .set('Content-Type', 'application/json') + .send('{"entityRefs":["a"]}'); + expect(entitiesCatalog.entitiesBatch).toHaveBeenCalledTimes(1); + expect(response.status).toEqual(200); + expect(response.body).toEqual({ items: [entity] }); + }); + }); + describe('GET /locations', () => { it('happy path: lists locations', async () => { const locations: Location[] = [ @@ -517,6 +550,7 @@ describe('createRouter readonly enabled', () => { beforeAll(async () => { entitiesCatalog = { entities: jest.fn(), + entitiesBatch: jest.fn(), removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), facets: jest.fn(), @@ -706,6 +740,7 @@ describe('NextRouter permissioning', () => { beforeAll(async () => { entitiesCatalog = { entities: jest.fn(), + entitiesBatch: jest.fn(), removeEntityByUid: jest.fn(), entityAncestry: jest.fn(), facets: jest.fn(), diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 5267519c33..4680dbb899 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -28,24 +28,25 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import yn from 'yn'; +import { z } from 'zod'; import { EntitiesCatalog } from '../catalog/types'; import { LocationAnalyzer } from '../ingestion/types'; +import { CatalogProcessingOrchestrator } from '../processing/types'; +import { validateEntityEnvelope } from '../processing/util'; import { basicEntityFilter, + entitiesBatchRequest, parseEntityFilterParams, parseEntityPaginationParams, parseEntityTransformParams, } from './request'; +import { parseEntityFacetParams } from './request/parseEntityFacetParams'; +import { LocationService, RefreshOptions, RefreshService } from './types'; import { disallowReadonlyMode, locationInput, validateRequestBody, } from './util'; -import { z } from 'zod'; -import { parseEntityFacetParams } from './request/parseEntityFacetParams'; -import { RefreshOptions, LocationService, RefreshService } from './types'; -import { CatalogProcessingOrchestrator } from '../processing/types'; -import { validateEntityEnvelope } from '../processing/util'; /** * Options used by {@link createRouter}. @@ -173,6 +174,16 @@ export async function createRouter( res.status(200).json(response); }, ) + .post('/entities/by-refs', async (req, res) => { + const request = entitiesBatchRequest(req); + const token = getBearerToken(req.header('authorization')); + const response = await entitiesCatalog.entitiesBatch({ + entityRefs: request.entityRefs, + fields: parseEntityTransformParams(req.query), + authorizationToken: token, + }); + res.status(200).json(response); + }) .get('/entity-facets', async (req, res) => { const response = await entitiesCatalog.facets({ filter: parseEntityFilterParams(req.query), diff --git a/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts b/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts new file mode 100644 index 0000000000..b3a91f9491 --- /dev/null +++ b/plugins/catalog-backend/src/service/request/entitiesBatchRequest.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { Request } from 'express'; +import { z } from 'zod'; + +const schema = z.object({ + entityRefs: z.array(z.string()), +}); + +export function entitiesBatchRequest(req: Request) { + try { + return schema.parse(req.body); + } catch (error) { + throw new InputError( + `Malformed request body (did you remember to specify an application/json content type?), ${error.message}`, + ); + } +} diff --git a/plugins/catalog-backend/src/service/request/index.ts b/plugins/catalog-backend/src/service/request/index.ts index 4ab579fbb6..0b09355e72 100644 --- a/plugins/catalog-backend/src/service/request/index.ts +++ b/plugins/catalog-backend/src/service/request/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export { entitiesBatchRequest } from './entitiesBatchRequest'; export { basicEntityFilter } from './basicEntityFilter'; export { parseEntityFilterParams } from './parseEntityFilterParams'; export { parseEntityPaginationParams } from './parseEntityPaginationParams'; diff --git a/plugins/catalog-backend/src/service/request/parseEntityFilterParams.ts b/plugins/catalog-backend/src/service/request/parseEntityFilterParams.ts index 1ea5d44b2c..767a36ce71 100644 --- a/plugins/catalog-backend/src/service/request/parseEntityFilterParams.ts +++ b/plugins/catalog-backend/src/service/request/parseEntityFilterParams.ts @@ -63,9 +63,13 @@ export function parseEntityFilterString( const equalsIndex = statement.indexOf('='); const key = - equalsIndex === -1 ? statement : statement.substr(0, equalsIndex).trim(); + equalsIndex === -1 + ? statement + : statement.substring(0, equalsIndex).trim(); const value = - equalsIndex === -1 ? undefined : statement.substr(equalsIndex + 1).trim(); + equalsIndex === -1 + ? undefined + : statement.substring(equalsIndex + 1).trim(); if (!key) { throw new InputError( `Invalid filter, '${statement}' is not a valid statement (expected a string on the form a=b or a= or a)`, diff --git a/plugins/catalog-backend/src/stitching/Stitcher.test.ts b/plugins/catalog-backend/src/stitching/Stitcher.test.ts index 9accebf1fc..a3657e45fd 100644 --- a/plugins/catalog-backend/src/stitching/Stitcher.test.ts +++ b/plugins/catalog-backend/src/stitching/Stitcher.test.ts @@ -29,7 +29,7 @@ import { Stitcher } from './Stitcher'; describe('Stitcher', () => { const databases = TestDatabases.create({ - ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], }); const logger = getVoidLogger(); @@ -115,13 +115,43 @@ describe('Stitcher', () => { const search = await db('search'); expect(search).toEqual( expect.arrayContaining([ - { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/other' }, - { entity_id: 'my-id', key: 'apiversion', value: 'a' }, - { entity_id: 'my-id', key: 'kind', value: 'k' }, - { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, - { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, - { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, - { entity_id: 'my-id', key: 'spec.k', value: 'v' }, + { + entity_id: 'my-id', + key: 'relations.looksat', + original_value: 'k:ns/other', + value: 'k:ns/other', + }, + { + entity_id: 'my-id', + key: 'apiversion', + original_value: 'a', + value: 'a', + }, + { entity_id: 'my-id', key: 'kind', original_value: 'k', value: 'k' }, + { + entity_id: 'my-id', + key: 'metadata.name', + original_value: 'n', + value: 'n', + }, + { + entity_id: 'my-id', + key: 'metadata.namespace', + original_value: 'ns', + value: 'ns', + }, + { + entity_id: 'my-id', + key: 'metadata.uid', + original_value: 'my-id', + value: 'my-id', + }, + { + entity_id: 'my-id', + key: 'spec.k', + original_value: 'v', + value: 'v', + }, ]), ); @@ -179,14 +209,49 @@ describe('Stitcher', () => { expect(await db('search')).toEqual( expect.arrayContaining([ - { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/other' }, - { entity_id: 'my-id', key: 'relations.looksat', value: 'k:ns/third' }, - { entity_id: 'my-id', key: 'apiversion', value: 'a' }, - { entity_id: 'my-id', key: 'kind', value: 'k' }, - { entity_id: 'my-id', key: 'metadata.name', value: 'n' }, - { entity_id: 'my-id', key: 'metadata.namespace', value: 'ns' }, - { entity_id: 'my-id', key: 'metadata.uid', value: 'my-id' }, - { entity_id: 'my-id', key: 'spec.k', value: 'v' }, + { + entity_id: 'my-id', + key: 'relations.looksat', + original_value: 'k:ns/other', + value: 'k:ns/other', + }, + { + entity_id: 'my-id', + key: 'relations.looksat', + original_value: 'k:ns/third', + value: 'k:ns/third', + }, + { + entity_id: 'my-id', + key: 'apiversion', + original_value: 'a', + value: 'a', + }, + { entity_id: 'my-id', key: 'kind', original_value: 'k', value: 'k' }, + { + entity_id: 'my-id', + key: 'metadata.name', + original_value: 'n', + value: 'n', + }, + { + entity_id: 'my-id', + key: 'metadata.namespace', + original_value: 'ns', + value: 'ns', + }, + { + entity_id: 'my-id', + key: 'metadata.uid', + original_value: 'my-id', + value: 'my-id', + }, + { + entity_id: 'my-id', + key: 'spec.k', + original_value: 'v', + value: 'v', + }, ]), ); }, diff --git a/plugins/catalog-backend/src/stitching/Stitcher.ts b/plugins/catalog-backend/src/stitching/Stitcher.ts index cf65a14fee..0da8e39694 100644 --- a/plugins/catalog-backend/src/stitching/Stitcher.ts +++ b/plugins/catalog-backend/src/stitching/Stitcher.ts @@ -22,7 +22,6 @@ import { } from '@backstage/catalog-model'; import { SerializedError, stringifyError } from '@backstage/errors'; import { Knex } from 'knex'; -import { uniqBy } from 'lodash'; import { v4 as uuid } from 'uuid'; import { Logger } from 'winston'; import { @@ -81,50 +80,43 @@ export class Stitcher { // one row (except in abnormal cases where the stitch was invoked for // something that didn't exist at all, in which case it's zero rows). // The join with the temporary incoming_references still gives one row. - // The only result set "expanding" join is the one with relations, so - // the output should be at least one row (if zero or one relations were - // found), or at most the same number of rows as relations. - const result: Array<{ - entityId: string; - processedEntity?: string; - errors: string; - incomingReferenceCount: string | number; - previousHash?: string; - relationType?: string; - relationTarget?: string; - }> = await this.database - .with('incoming_references', function incomingReferences(builder) { - return builder - .from('refresh_state_references') - .where({ target_entity_ref: entityRef }) - .count({ count: '*' }); - }) - .select({ - entityId: 'refresh_state.entity_id', - processedEntity: 'refresh_state.processed_entity', - errors: 'refresh_state.errors', - incomingReferenceCount: 'incoming_references.count', - previousHash: 'final_entities.hash', - relationType: 'relations.type', - relationTarget: 'relations.target_entity_ref', - }) - .from('refresh_state') - .where({ 'refresh_state.entity_ref': entityRef }) - .crossJoin(this.database.raw('incoming_references')) - .leftOuterJoin('final_entities', { - 'final_entities.entity_id': 'refresh_state.entity_id', - }) - .leftOuterJoin('relations', { - 'relations.source_entity_ref': 'refresh_state.entity_ref', - }) - .orderBy('relationType', 'asc') - .orderBy('relationTarget', 'asc'); + const [processedResult, relationsResult] = await Promise.all([ + this.database + .with('incoming_references', function incomingReferences(builder) { + return builder + .from('refresh_state_references') + .where({ target_entity_ref: entityRef }) + .count({ count: '*' }); + }) + .select({ + entityId: 'refresh_state.entity_id', + processedEntity: 'refresh_state.processed_entity', + errors: 'refresh_state.errors', + incomingReferenceCount: 'incoming_references.count', + previousHash: 'final_entities.hash', + }) + .from('refresh_state') + .where({ 'refresh_state.entity_ref': entityRef }) + .crossJoin(this.database.raw('incoming_references')) + .leftOuterJoin('final_entities', { + 'final_entities.entity_id': 'refresh_state.entity_id', + }), + this.database + .distinct({ + relationType: 'type', + relationTarget: 'target_entity_ref', + }) + .from('relations') + .where({ source_entity_ref: entityRef }) + .orderBy('relationType', 'asc') + .orderBy('relationTarget', 'asc'), + ]); // If there were no rows returned, it would mean that there was no // matching row even in the refresh_state. This can happen for example // if we emit a relation to something that hasn't been ingested yet. // It's safe to ignore this stitch attempt in that case. - if (!result.length) { + if (!processedResult.length) { this.logger.error( `Unable to stitch ${entityRef}, item does not exist in refresh state table`, ); @@ -137,7 +129,7 @@ export class Stitcher { errors, incomingReferenceCount, previousHash, - } = result[0]; + } = processedResult[0]; // If there was no processed entity in place, the target hasn't been // through the processing steps yet. It's safe to ignore this stitch @@ -177,11 +169,7 @@ export class Stitcher { // TODO: entityRef is lower case and should be uppercase in the final // result - const uniqueRelationRows = uniqBy( - result, - r => `${r.relationType}:${r.relationTarget}`, - ); - entity.relations = uniqueRelationRows + entity.relations = relationsResult .filter(row => row.relationType /* exclude null row, if relevant */) .map(row => ({ type: row.relationType!, diff --git a/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts b/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts index 7364486f06..384794d230 100644 --- a/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts +++ b/plugins/catalog-backend/src/stitching/buildEntitySearch.test.ts @@ -88,19 +88,26 @@ describe('buildEntitySearch', () => { ]; const output = mapToRows(input, 'eid'); expect(output).toEqual([ - { entity_id: 'eid', key: 'a', value: 'true' }, - { entity_id: 'eid', key: 'b', value: 'false' }, - { entity_id: 'eid', key: 'c', value: '7' }, - { entity_id: 'eid', key: 'd', value: 'string' }, - { entity_id: 'eid', key: 'e', value: null }, - { entity_id: 'eid', key: 'f', value: null }, + { entity_id: 'eid', key: 'a', original_value: 'true', value: 'true' }, + { entity_id: 'eid', key: 'b', original_value: 'false', value: 'false' }, + { entity_id: 'eid', key: 'c', original_value: '7', value: '7' }, + { + entity_id: 'eid', + key: 'd', + original_value: 'string', + value: 'string', + }, + { entity_id: 'eid', key: 'e', original_value: null, value: null }, + { entity_id: 'eid', key: 'f', original_value: null, value: null }, ]); }); - it('emits lowercase version of keys and values', () => { + it('emits lowercase version of keys and values and also keeps the original value', () => { const input = [{ key: 'fOo', value: 'BaR' }]; const output = mapToRows(input, 'eid'); - expect(output).toEqual([{ entity_id: 'eid', key: 'foo', value: 'bar' }]); + expect(output).toEqual([ + { entity_id: 'eid', key: 'foo', original_value: 'BaR', value: 'bar' }, + ]); }); it('skips very large keys', () => { @@ -112,7 +119,9 @@ describe('buildEntitySearch', () => { it('replaces very large values with null', () => { const input = [{ key: 'foo', value: 'a'.repeat(10000) }]; const output = mapToRows(input, 'eid'); - expect(output).toEqual([{ entity_id: 'eid', key: 'foo', value: null }]); + expect(output).toEqual([ + { entity_id: 'eid', key: 'foo', original_value: null, value: null }, + ]); }); }); @@ -124,14 +133,35 @@ describe('buildEntitySearch', () => { metadata: { name: 'n' }, }; expect(buildEntitySearch('eid', input)).toEqual([ - { entity_id: 'eid', key: 'apiversion', value: 'a' }, - { entity_id: 'eid', key: 'kind', value: 'b' }, - { entity_id: 'eid', key: 'metadata.name', value: 'n' }, - { entity_id: 'eid', key: 'metadata.namespace', value: null }, - { entity_id: 'eid', key: 'metadata.uid', value: null }, + { + entity_id: 'eid', + key: 'apiversion', + original_value: 'a', + value: 'a', + }, + { entity_id: 'eid', key: 'kind', original_value: 'b', value: 'b' }, + { + entity_id: 'eid', + key: 'metadata.name', + original_value: 'n', + value: 'n', + }, { entity_id: 'eid', key: 'metadata.namespace', + original_value: null, + value: null, + }, + { + entity_id: 'eid', + key: 'metadata.uid', + original_value: null, + value: null, + }, + { + entity_id: 'eid', + key: 'metadata.namespace', + original_value: DEFAULT_NAMESPACE, value: DEFAULT_NAMESPACE, }, ]); @@ -154,18 +184,49 @@ describe('buildEntitySearch', () => { metadata: { name: 'n' }, }; expect(buildEntitySearch('eid', input)).toEqual([ - { entity_id: 'eid', key: 'apiversion', value: 'a' }, - { entity_id: 'eid', key: 'kind', value: 'b' }, - { entity_id: 'eid', key: 'metadata.name', value: 'n' }, - { entity_id: 'eid', key: 'metadata.namespace', value: null }, - { entity_id: 'eid', key: 'metadata.uid', value: null }, + { + entity_id: 'eid', + key: 'apiversion', + original_value: 'a', + value: 'a', + }, + { entity_id: 'eid', key: 'kind', original_value: 'b', value: 'b' }, + { + entity_id: 'eid', + key: 'metadata.name', + original_value: 'n', + value: 'n', + }, { entity_id: 'eid', key: 'metadata.namespace', + original_value: null, + value: null, + }, + { + entity_id: 'eid', + key: 'metadata.uid', + original_value: null, + value: null, + }, + { + entity_id: 'eid', + key: 'metadata.namespace', + original_value: DEFAULT_NAMESPACE, value: DEFAULT_NAMESPACE, }, - { entity_id: 'eid', key: 'relations.t1', value: 'k:ns/a' }, - { entity_id: 'eid', key: 'relations.t2', value: 'k:ns/b' }, + { + entity_id: 'eid', + key: 'relations.t1', + original_value: 'k:ns/a', + value: 'k:ns/a', + }, + { + entity_id: 'eid', + key: 'relations.t2', + original_value: 'k:ns/b', + value: 'k:ns/b', + }, ]); }); diff --git a/plugins/catalog-backend/src/stitching/buildEntitySearch.ts b/plugins/catalog-backend/src/stitching/buildEntitySearch.ts index 88a87df134..b97083f207 100644 --- a/plugins/catalog-backend/src/stitching/buildEntitySearch.ts +++ b/plugins/catalog-backend/src/stitching/buildEntitySearch.ts @@ -134,15 +134,30 @@ export function mapToRows(input: Kv[], entityId: string): DbSearchRow[] { for (const { key: rawKey, value: rawValue } of input) { const key = rawKey.toLocaleLowerCase('en-US'); if (rawValue === undefined || rawValue === null) { - result.push({ entity_id: entityId, key, value: null }); + result.push({ + entity_id: entityId, + key, + original_value: null, + value: null, + }); } else { const value = String(rawValue).toLocaleLowerCase('en-US'); if (key.length <= MAX_KEY_LENGTH) { - result.push({ - entity_id: entityId, - key, - value: value.length <= MAX_VALUE_LENGTH ? value : null, - }); + if (value.length <= MAX_VALUE_LENGTH) { + result.push({ + entity_id: entityId, + key, + original_value: String(rawValue), + value: value, + }); + } else { + result.push({ + entity_id: entityId, + key, + original_value: null, + value: null, + }); + } } } } diff --git a/plugins/catalog-backend/src/util/conversion.ts b/plugins/catalog-backend/src/util/conversion.ts index 83302722d2..0b92b88d90 100644 --- a/plugins/catalog-backend/src/util/conversion.ts +++ b/plugins/catalog-backend/src/util/conversion.ts @@ -23,7 +23,7 @@ import { stringifyLocationRef, } from '@backstage/catalog-model'; import { createHash } from 'crypto'; -import { LocationSpec } from '@backstage/plugin-catalog-node'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; export function locationSpecToMetadataName(location: LocationSpec) { const hash = createHash('sha1') diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index d1378e9418..a11186f0fc 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-catalog-common +## 1.0.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.0.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 1.0.8 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/plugin-search-common@1.1.1 + ## 1.0.8-next.0 ### Patch Changes diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 8af6d06c5a..b0be0cc01e 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-common", "description": "Common functionalities for the catalog plugin", - "version": "1.0.8-next.0", + "version": "1.0.9-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-customized/CHANGELOG.md b/plugins/catalog-customized/CHANGELOG.md index 04be429ab6..d579b84de4 100644 --- a/plugins/catalog-customized/CHANGELOG.md +++ b/plugins/catalog-customized/CHANGELOG.md @@ -1,5 +1,37 @@ # @internal/plugin-catalog-customized +## 0.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.6.2-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + +## 0.0.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/plugin-catalog@1.6.2-next.0 + +## 0.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/plugin-catalog@1.6.1 + +## 0.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.6.1-next.1 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.0.4-next.0 ### Patch Changes diff --git a/plugins/catalog-customized/package.json b/plugins/catalog-customized/package.json index 537180f05e..32c8406320 100644 --- a/plugins/catalog-customized/package.json +++ b/plugins/catalog-customized/package.json @@ -1,7 +1,7 @@ { "name": "@internal/plugin-catalog-customized", "description": "The internal Backstage Customizable plugin for browsing the Backstage catalog", - "version": "0.0.4-next.0", + "version": "0.0.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-graph/.eslintrc.js b/plugins/catalog-graph/.eslintrc.js index e2a53a6ad2..45bb6db521 100644 --- a/plugins/catalog-graph/.eslintrc.js +++ b/plugins/catalog-graph/.eslintrc.js @@ -1 +1,5 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'testing-library/prefer-screen-queries': 'error', + }, +}); diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 7f8e1e0277..2c42f6d714 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,73 @@ # @backstage/plugin-catalog-graph +## 0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.2.24-next.0 + +### Patch Changes + +- cb716004ef: Internal refactor to improve tests +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.2.23 + +### Patch Changes + +- da0bf25d1a: Preserve graph options and increment `maxDepth` by 1. + + The change will preserve options used at the `CatalogGraphCard` + (displayed at the entity page) and additionally, increments the + `maxDepth` option by 1 to increase the scope slightly compared to + the graph already seen by the users. + + The default for `maxDepth` at `CatalogGraphCard` is 1. + +- 21d84ef332: feat: Enable theme overrides for components in catalog-graph plugin +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/theme@0.2.16 + +## 0.2.23-next.1 + +### Patch Changes + +- da0bf25d1a: Preserve graph options and increment `maxDepth` by 1. + + The change will preserve options used at the `CatalogGraphCard` + (displayed at the entity page) and additionally, increments the + `maxDepth` option by 1 to increase the scope slightly compared to + the graph already seen by the users. + + The default for `maxDepth` at `CatalogGraphCard` is 1. + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.2.23-next.0 ### Patch Changes diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 3b78d5d7ba..ef30239045 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.23-next.0", + "version": "0.2.24-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 74fcd4e501..27b67102e4 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity } from '@backstage/catalog-model'; import { ApiProvider } from '@backstage/core-app-api'; import { analyticsApiRef } from '@backstage/core-plugin-api'; import { - CatalogApi, catalogApiRef, EntityProvider, entityRouteRef, @@ -28,6 +28,7 @@ import { TestApiProvider, TestApiRegistry, } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { catalogGraphRouteRef } from '../../routes'; @@ -36,10 +37,24 @@ import { CatalogGraphCard } from './CatalogGraphCard'; describe('', () => { let entity: Entity; let wrapper: JSX.Element; - let catalog: jest.Mocked; + const catalog = { + getEntities: jest.fn(), + getEntityByRef: jest.fn(), + removeEntityByUid: jest.fn(), + getLocationById: jest.fn(), + getLocationByRef: jest.fn(), + addLocation: jest.fn(), + removeLocationById: jest.fn(), + refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), + validateEntity: jest.fn(), + }; let apis: TestApiRegistry; beforeEach(() => { + jest.clearAllMocks(); + entity = { apiVersion: 'a', kind: 'b', @@ -48,19 +63,6 @@ describe('', () => { namespace: 'd', }, }; - catalog = { - getEntities: jest.fn(), - getEntityByRef: jest.fn(async _ => ({ ...entity, relations: [] })), - removeEntityByUid: jest.fn(), - getLocationById: jest.fn(), - getLocationByRef: jest.fn(), - addLocation: jest.fn(), - removeLocationById: jest.fn(), - refreshEntity: jest.fn(), - getEntityAncestors: jest.fn(), - getEntityFacets: jest.fn(), - validateEntity: jest.fn(), - }; apis = TestApiRegistry.from([catalogApiRef, catalog]); wrapper = ( @@ -73,20 +75,30 @@ describe('', () => { }); test('renders without exploding', async () => { - const { findByText, findAllByTestId } = await renderInTestApp(wrapper, { + catalog.getEntityByRef.mockImplementation(async _ => ({ + ...entity, + relations: [], + })); + + await renderInTestApp(wrapper, { mountedRoutes: { '/entity/{kind}/{namespace}/{name}': entityRouteRef, '/catalog-graph': catalogGraphRouteRef, }, }); - expect(await findByText('b:d/c')).toBeInTheDocument(); - expect(await findAllByTestId('node')).toHaveLength(1); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + expect(await screen.findAllByTestId('node')).toHaveLength(1); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); }); test('renders with custom title', async () => { - const { findByText } = await renderInTestApp( + catalog.getEntityByRef.mockImplementation(async _ => ({ + ...entity, + relations: [], + })); + + await renderInTestApp( @@ -100,29 +112,63 @@ describe('', () => { }, ); - expect(await findByText('Custom Title')).toBeInTheDocument(); + expect(await screen.findByText('Custom Title')).toBeInTheDocument(); }); test('renders link to standalone viewer', async () => { - const { findByText, getByText } = await renderInTestApp(wrapper, { + catalog.getEntityByRef.mockImplementation(async _ => ({ + ...entity, + relations: [], + })); + + await renderInTestApp(wrapper, { mountedRoutes: { '/entity/{kind}/{namespace}/{name}': entityRouteRef, '/catalog-graph': catalogGraphRouteRef, }, }); - expect(await findByText('b:d/c')).toBeInTheDocument(); - const button = getByText('View graph'); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + const button = screen.getByText('View graph'); expect(button).toBeInTheDocument(); expect(button.closest('a')).toHaveAttribute( 'href', - '/catalog-graph?rootEntityRefs%5B%5D=b%3Ad%2Fc', + '/catalog-graph?rootEntityRefs%5B%5D=b%3Ad%2Fc&maxDepth=2&unidirectional=true&mergeRelations=true&direction=LR', + ); + }); + + test('renders link to standalone viewer with custom config', async () => { + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/entity/{kind}/{namespace}/{name}': entityRouteRef, + '/catalog-graph': catalogGraphRouteRef, + }, + }, + ); + + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + const button = screen.getByText('View graph'); + expect(button).toBeInTheDocument(); + expect(button.closest('a')).toHaveAttribute( + 'href', + '/catalog-graph?rootEntityRefs%5B%5D=b%3Ad%2Fc&maxDepth=3&unidirectional=true&mergeRelations=false&direction=LR', ); }); test('captures analytics event on click', async () => { + catalog.getEntityByRef.mockImplementation(async _ => ({ + ...entity, + relations: [], + })); + const analyticsSpy = new MockAnalyticsApi(); - const { findByText } = await renderInTestApp( + await renderInTestApp( {wrapper} , @@ -134,8 +180,8 @@ describe('', () => { }, ); - expect(await findByText('b:d/c')).toBeInTheDocument(); - await userEvent.click(await findByText('b:d/c')); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + await userEvent.click(await screen.findByText('b:d/c')); expect(analyticsSpy.getEvents()[0]).toMatchObject({ action: 'click', diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index ae1ca9c22d..40ee90d784 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -39,18 +39,21 @@ import { RelationPairs, } from '../EntityRelationsGraph'; -const useStyles = makeStyles({ - card: ({ height }) => ({ - display: 'flex', - flexDirection: 'column', - maxHeight: height, - minHeight: height, - }), - graph: { - flex: 1, - minHeight: 0, +const useStyles = makeStyles( + { + card: ({ height }) => ({ + display: 'flex', + flexDirection: 'column', + maxHeight: height, + minHeight: height, + }), + graph: { + flex: 1, + minHeight: 0, + }, }, -}); + { name: 'PluginCatalogGraphCatalogGraphCard' }, +); export const CatalogGraphCard = (props: { variant?: InfoCardVariants; @@ -106,7 +109,15 @@ export const CatalogGraphCard = (props: { ); const catalogGraphParams = qs.stringify( - { rootEntityRefs: [stringifyEntityRef(entity)] }, + { + rootEntityRefs: [stringifyEntityRef(entity)], + maxDepth: maxDepth + 1, + unidirectional, + mergeRelations, + kinds, + relations, + direction, + }, { arrayFormat: 'brackets', addQueryPrefix: true }, ); const catalogGraphUrl = `${catalogGraphRoute()}${catalogGraphParams}`; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index 9cca6ef9be..f7e1a9fbe7 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -13,18 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { RELATION_HAS_PART, RELATION_PART_OF } from '@backstage/catalog-model'; import { analyticsApiRef } from '@backstage/core-plugin-api'; -import { - CatalogApi, - catalogApiRef, - entityRouteRef, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { MockAnalyticsApi, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { CatalogGraphPage } from './CatalogGraphPage'; @@ -38,63 +36,59 @@ jest.mock('react-router', () => ({ describe('', () => { let wrapper: JSX.Element; - let catalog: jest.Mocked; + const entityC = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + namespace: 'd', + }, + relations: [ + { + type: RELATION_PART_OF, + targetRef: 'b:d/e', + target: { + kind: 'b', + namespace: 'd', + name: 'e', + }, + }, + ], + }; + const entityE = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'e', + namespace: 'd', + }, + relations: [ + { + type: RELATION_HAS_PART, + targetRef: 'b:d/c', + target: { + kind: 'b', + namespace: 'd', + name: 'c', + }, + }, + ], + }; + const catalog = { + getEntities: jest.fn(), + getEntityByRef: jest.fn(), + removeEntityByUid: jest.fn(), + getLocationById: jest.fn(), + getLocationByRef: jest.fn(), + addLocation: jest.fn(), + removeLocationById: jest.fn(), + refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), + validateEntity: jest.fn(), + }; beforeEach(() => { - const entityC = { - apiVersion: 'a', - kind: 'b', - metadata: { - name: 'c', - namespace: 'd', - }, - relations: [ - { - type: RELATION_PART_OF, - targetRef: 'b:d/e', - target: { - kind: 'b', - namespace: 'd', - name: 'e', - }, - }, - ], - }; - const entityE = { - apiVersion: 'a', - kind: 'b', - metadata: { - name: 'e', - namespace: 'd', - }, - relations: [ - { - type: RELATION_HAS_PART, - targetRef: 'b:d/c', - target: { - kind: 'b', - namespace: 'd', - name: 'c', - }, - }, - ], - }; - catalog = { - getEntities: jest.fn(), - getEntityByRef: jest.fn(async (n: any) => - n === 'b:d/e' ? entityE : entityC, - ), - removeEntityByUid: jest.fn(), - getLocationById: jest.fn(), - getLocationByRef: jest.fn(), - addLocation: jest.fn(), - removeLocationById: jest.fn(), - refreshEntity: jest.fn(), - getEntityAncestors: jest.fn(), - getEntityFacets: jest.fn(), - validateEntity: jest.fn(), - }; - wrapper = ( ', () => { afterEach(() => jest.resetAllMocks()); test('should render without exploding', async () => { - const { getByText, findByText, findAllByTestId } = await renderInTestApp( - wrapper, - { - mountedRoutes: { - '/entity/{kind}/{namespace}/{name}': entityRouteRef, - }, - }, + catalog.getEntityByRef.mockImplementation(async (n: any) => + n === 'b:d/e' ? entityE : entityC, ); - expect(getByText('Catalog Graph')).toBeInTheDocument(); - expect(await findByText('b:d/c')).toBeInTheDocument(); - expect(await findByText('b:d/e')).toBeInTheDocument(); - expect(await findAllByTestId('node')).toHaveLength(2); + await renderInTestApp(wrapper, { + mountedRoutes: { + '/entity/{kind}/{namespace}/{name}': entityRouteRef, + }, + }); + + expect(screen.getByText('Catalog Graph')).toBeInTheDocument(); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + expect(await screen.findByText('b:d/e')).toBeInTheDocument(); + expect(await screen.findAllByTestId('node')).toHaveLength(2); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(2); }); test('should toggle filters', async () => { - const { getByText, queryByText } = await renderInTestApp(wrapper, { + catalog.getEntityByRef.mockImplementation(async (n: any) => + n === 'b:d/e' ? entityE : entityC, + ); + + await renderInTestApp(wrapper, { mountedRoutes: { '/entity/{kind}/{namespace}/{name}': entityRouteRef, }, }); - expect(queryByText('Max Depth')).toBeNull(); + expect(screen.queryByText('Max Depth')).toBeNull(); - await userEvent.click(getByText('Filters')); + await userEvent.click(screen.getByText('Filters')); - expect(getByText('Max Depth')).toBeInTheDocument(); + expect(screen.getByText('Max Depth')).toBeInTheDocument(); }); test('should select other entity', async () => { - const { getByText, findByText, findAllByTestId } = await renderInTestApp( - wrapper, - { - mountedRoutes: { - '/entity/{kind}/{namespace}/{name}': entityRouteRef, - }, - }, + catalog.getEntityByRef.mockImplementation(async (n: any) => + n === 'b:d/e' ? entityE : entityC, ); - expect(await findAllByTestId('node')).toHaveLength(2); - - await userEvent.click(getByText('b:d/e')); - - expect(await findByText('hasPart')).toBeInTheDocument(); - }); - - test('should navigate to entity', async () => { - const { getByText, findAllByTestId } = await renderInTestApp(wrapper, { + await renderInTestApp(wrapper, { mountedRoutes: { '/entity/{kind}/{namespace}/{name}': entityRouteRef, }, }); - expect(await findAllByTestId('node')).toHaveLength(2); + expect(await screen.findAllByTestId('node')).toHaveLength(2); + + await userEvent.click(screen.getByText('b:d/e')); + + expect(await screen.findByText('hasPart')).toBeInTheDocument(); + }); + + test('should navigate to entity', async () => { + catalog.getEntityByRef.mockImplementation(async (n: any) => + n === 'b:d/e' ? entityE : entityC, + ); + + await renderInTestApp(wrapper, { + mountedRoutes: { + '/entity/{kind}/{namespace}/{name}': entityRouteRef, + }, + }); + + expect(await screen.findAllByTestId('node')).toHaveLength(2); const user = userEvent.setup(); await user.keyboard('{Shift>}'); - await user.click(getByText('b:d/e')); + await user.click(screen.getByText('b:d/e')); expect(navigate).toHaveBeenCalledWith('/entity/{kind}/{namespace}/{name}'); }); test('should capture analytics event when selecting other entity', async () => { + catalog.getEntityByRef.mockImplementation(async (n: any) => + n === 'b:d/e' ? entityE : entityC, + ); + const analyticsSpy = new MockAnalyticsApi(); - const { getByText, findAllByTestId } = await renderInTestApp( + await renderInTestApp( {wrapper} , @@ -186,12 +194,12 @@ describe('', () => { }, ); - expect(await findAllByTestId('node')).toHaveLength(2); + expect(await screen.findAllByTestId('node')).toHaveLength(2); // We wait a bit here to reliably reproduce an issue where that requires the `baseVal` and `view` mocks await new Promise(r => setTimeout(r, 100)); - await userEvent.click(getByText('b:d/e')); + await userEvent.click(screen.getByText('b:d/e')); expect(analyticsSpy.getEvents()[0]).toMatchObject({ action: 'click', @@ -200,8 +208,12 @@ describe('', () => { }); test('should capture analytics event when navigating to entity', async () => { + catalog.getEntityByRef.mockImplementation(async (n: any) => + n === 'b:d/e' ? entityE : entityC, + ); + const analyticsSpy = new MockAnalyticsApi(); - const { getByText, findAllByTestId } = await renderInTestApp( + await renderInTestApp( {wrapper} , @@ -212,11 +224,11 @@ describe('', () => { }, ); - expect(await findAllByTestId('node')).toHaveLength(2); + expect(await screen.findAllByTestId('node')).toHaveLength(2); const user = userEvent.setup(); await user.keyboard('{Shift>}'); - await user.click(getByText('b:d/e')); + await user.click(screen.getByText('b:d/e')); expect(analyticsSpy.getEvents()[0]).toMatchObject({ action: 'click', diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index a6a6c2a711..4a36d516e2 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -48,57 +48,60 @@ import { SelectedRelationsFilter } from './SelectedRelationsFilter'; import { SwitchFilter } from './SwitchFilter'; import { useCatalogGraphPage } from './useCatalogGraphPage'; -const useStyles = makeStyles(theme => ({ - content: { - minHeight: 0, - }, - container: { - height: '100%', - maxHeight: '100%', - minHeight: 0, - }, - fullHeight: { - maxHeight: '100%', - display: 'flex', - minHeight: 0, - }, - graphWrapper: { - position: 'relative', - flex: 1, - minHeight: 0, - display: 'flex', - }, - graph: { - flex: 1, - minHeight: 0, - }, - legend: { - position: 'absolute', - bottom: 0, - right: 0, - padding: theme.spacing(1), - '& .icon': { - verticalAlign: 'bottom', +const useStyles = makeStyles( + theme => ({ + content: { + minHeight: 0, }, - }, - filters: { - display: 'grid', - gridGap: theme.spacing(1), - gridAutoRows: 'auto', - [theme.breakpoints.up('lg')]: { - display: 'block', + container: { + height: '100%', + maxHeight: '100%', + minHeight: 0, }, - [theme.breakpoints.only('md')]: { - gridTemplateColumns: 'repeat(3, 1fr)', + fullHeight: { + maxHeight: '100%', + display: 'flex', + minHeight: 0, }, - [theme.breakpoints.only('sm')]: { - gridTemplateColumns: 'repeat(2, 1fr)', + graphWrapper: { + position: 'relative', + flex: 1, + minHeight: 0, + display: 'flex', }, - [theme.breakpoints.down('xs')]: { - gridTemplateColumns: 'repeat(1, 1fr)', + graph: { + flex: 1, + minHeight: 0, }, - }, -})); + legend: { + position: 'absolute', + bottom: 0, + right: 0, + padding: theme.spacing(1), + '& .icon': { + verticalAlign: 'bottom', + }, + }, + filters: { + display: 'grid', + gridGap: theme.spacing(1), + gridAutoRows: 'auto', + [theme.breakpoints.up('lg')]: { + display: 'block', + }, + [theme.breakpoints.only('md')]: { + gridTemplateColumns: 'repeat(3, 1fr)', + }, + [theme.breakpoints.only('sm')]: { + gridTemplateColumns: 'repeat(2, 1fr)', + }, + [theme.breakpoints.down('xs')]: { + gridTemplateColumns: 'repeat(1, 1fr)', + }, + }, + }), + { name: 'PluginCatalogGraphCatalogGraphPage' }, +); export const CatalogGraphPage = (props: { relationPairs?: RelationPairs; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx index 0be0fca66b..85d9d97c7f 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { render, waitFor } from '@testing-library/react'; + +import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { CurveFilter } from './CurveFilter'; @@ -21,26 +22,22 @@ import { CurveFilter } from './CurveFilter'; describe('', () => { test('should display current curve label', () => { const onChange = jest.fn(); - const { getByText } = render( - , - ); + render(); - expect(getByText('Monotone X')).toBeInTheDocument(); + expect(screen.getByText('Monotone X')).toBeInTheDocument(); }); test('should select an alternative curve factory', async () => { const onChange = jest.fn(); - const { getByText, getByTestId } = render( - , - ); + render(); - expect(getByText('Step Before')).toBeInTheDocument(); + expect(screen.getByText('Step Before')).toBeInTheDocument(); - await userEvent.click(getByTestId('select')); - await userEvent.click(getByText('Monotone X')); + await userEvent.click(screen.getByTestId('select')); + await userEvent.click(screen.getByText('Monotone X')); await waitFor(() => { - expect(getByText('Monotone X')).toBeInTheDocument(); + expect(screen.getByText('Monotone X')).toBeInTheDocument(); expect(onChange).toHaveBeenCalledWith('curveMonotoneX'); }); }); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx index 05c071f1f9..cb660b5a25 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { render, waitFor } from '@testing-library/react'; + +import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { Direction } from '../EntityRelationsGraph'; @@ -21,26 +22,26 @@ import { DirectionFilter } from './DirectionFilter'; describe('', () => { test('should display current value', () => { - const { getByText } = render( + render( {}} />, ); - expect(getByText('Left to right')).toBeInTheDocument(); + expect(screen.getByText('Left to right')).toBeInTheDocument(); }); test('should select direction', async () => { const onChange = jest.fn(); - const { getByText, getByTestId } = render( + render( , ); - expect(getByText('Right to left')).toBeInTheDocument(); + expect(screen.getByText('Right to left')).toBeInTheDocument(); - await userEvent.click(getByTestId('select')); - await userEvent.click(getByText('Top to bottom')); + await userEvent.click(screen.getByTestId('select')); + await userEvent.click(screen.getByText('Top to bottom')); await waitFor(() => { - expect(getByText('Top to bottom')).toBeInTheDocument(); + expect(screen.getByText('Top to bottom')).toBeInTheDocument(); expect(onChange).toHaveBeenCalledWith(Direction.TOP_BOTTOM); }); }); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.test.tsx index 3f3a7f82a6..89f3169981 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.test.tsx @@ -13,64 +13,65 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { render, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; + +import { render, screen } from '@testing-library/react'; +import user from '@testing-library/user-event'; import React from 'react'; import { MaxDepthFilter } from './MaxDepthFilter'; describe('', () => { test('should display current value', () => { - const { getByLabelText } = render( - {}} />, - ); + render( {}} />); - expect(getByLabelText('maxp')).toBeInTheDocument(); - expect(getByLabelText('maxp')).toHaveValue(5); + expect(screen.getByLabelText('maxp')).toBeInTheDocument(); + expect(screen.getByLabelText('maxp')).toHaveValue(5); }); test('should display infinite if non finite', () => { - const { getByPlaceholderText, getByLabelText } = render( + render( {}} />, ); - expect(getByPlaceholderText(/Infinite/)).toBeInTheDocument(); - expect(getByLabelText('maxp')).toHaveValue(null); + expect(screen.getByPlaceholderText(/Infinite/)).toBeInTheDocument(); + expect(screen.getByLabelText('maxp')).toHaveValue(null); }); test('should clear max depth', async () => { const onChange = jest.fn(); - const { getByLabelText } = render( - , - ); + render(); - await userEvent.click(getByLabelText('clear max depth')); + expect(onChange).not.toHaveBeenCalled(); + await user.click(screen.getByLabelText('clear max depth')); expect(onChange).toHaveBeenCalledWith(Number.POSITIVE_INFINITY); }); test('should set max depth to undefined if below one', async () => { const onChange = jest.fn(); - const { getByLabelText } = render( - , - ); + render(); - await userEvent.clear(getByLabelText('maxp')); - await userEvent.type(getByLabelText('maxp'), '0'); + await user.clear(screen.getByLabelText('maxp')); + await user.type(screen.getByLabelText('maxp'), '0'); expect(onChange).toHaveBeenCalledWith(Number.POSITIVE_INFINITY); }); test('should select direction', async () => { - const onChange = jest.fn(); - const { getByLabelText } = render( - , + let value = 5; + render( + { + value = v; + }} + />, ); - expect(getByLabelText('maxp')).toHaveValue(5); + expect(screen.getByLabelText('maxp')).toHaveValue(5); + expect(value).toBe(5); - await userEvent.clear(getByLabelText('maxp')); - await userEvent.type(getByLabelText('maxp'), '10'); - waitFor(() => { - expect(onChange).toHaveBeenCalledWith(10); - }); + await user.clear(screen.getByLabelText('maxp')); + expect(value).toBe(Number.POSITIVE_INFINITY); + await user.type(screen.getByLabelText('maxp'), '10'); + expect(value).toBe(10); }); }); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.tsx index 7828a60fb6..e6fb2fbab4 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.tsx @@ -23,34 +23,56 @@ import { Typography, } from '@material-ui/core'; import ClearIcon from '@material-ui/icons/Clear'; -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; export type Props = { value: number; onChange: (value: number) => void; }; -const useStyles = makeStyles({ - formControl: { - width: '100%', - maxWidth: 300, +const useStyles = makeStyles( + { + formControl: { + width: '100%', + maxWidth: 300, + }, }, -}); + { name: 'PluginCatalogGraphMaxDepthFilter' }, +); export const MaxDepthFilter = ({ value, onChange }: Props) => { const classes = useStyles(); + const onChangeRef = useRef(onChange); + const [currentValue, setCurrentValue] = useState(value); + // Keep a fresh reference to the latest callback + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + + // If the value changes externally, update ourselves + useEffect(() => { + setCurrentValue(value); + }, [value]); + + // When the entered text changes, update ourselves and communicate externally const handleChange = useCallback( (event: React.ChangeEvent) => { - const v = Number(event.target.value); - onChange(v <= 0 ? Number.POSITIVE_INFINITY : v); + const newValueNumeric = Number(event.target.value); + const newValue = + Number.isFinite(newValueNumeric) && newValueNumeric > 0 + ? newValueNumeric + : Number.POSITIVE_INFINITY; + setCurrentValue(newValue); + onChangeRef.current(newValue); }, - [onChange], + [], ); const reset = useCallback(() => { - onChange(Number.POSITIVE_INFINITY); - }, [onChange]); + setCurrentValue(Number.POSITIVE_INFINITY); + onChangeRef.current(Number.POSITIVE_INFINITY); + }, [onChangeRef]); return ( @@ -59,7 +81,7 @@ export const MaxDepthFilter = ({ value, onChange }: Props) => { diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx index 580c118b27..511592af0c 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { GetEntityFacetsResponse } from '@backstage/catalog-client'; import { ApiProvider } from '@backstage/core-app-api'; import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { SelectedKindsFilter } from './SelectedKindsFilter'; @@ -42,37 +43,37 @@ const apis = TestApiRegistry.from( describe('', () => { it('should not explode while loading', async () => { - const rendered = await renderWithEffects( + const { baseElement } = await renderWithEffects( {}} /> , ); - expect(rendered.baseElement).toBeInTheDocument(); + expect(baseElement).toBeInTheDocument(); }); it('should render current value', async () => { - const rendered = await renderWithEffects( + await renderWithEffects( {}} /> , ); - expect(rendered.getByText('API')).toBeInTheDocument(); - expect(rendered.getByText('Component')).toBeInTheDocument(); + expect(screen.getByText('API')).toBeInTheDocument(); + expect(screen.getByText('Component')).toBeInTheDocument(); }); it('should select value', async () => { const onChange = jest.fn(); - const { getByLabelText, getByText } = await renderWithEffects( + await renderWithEffects( , ); - await userEvent.click(getByLabelText('Open')); - await waitFor(() => expect(getByText('System')).toBeInTheDocument()); + await userEvent.click(screen.getByLabelText('Open')); + await waitFor(() => expect(screen.getByText('System')).toBeInTheDocument()); - await userEvent.click(getByText('System')); + await userEvent.click(screen.getByText('System')); await waitFor(() => { expect(onChange).toHaveBeenCalledWith(['api', 'component', 'system']); @@ -81,7 +82,7 @@ describe('', () => { it('should return undefined if all values are selected', async () => { const onChange = jest.fn(); - const { getByLabelText, getByText } = await renderWithEffects( + await renderWithEffects( ', () => { /> , ); - await userEvent.click(getByLabelText('Open')); + await userEvent.click(screen.getByLabelText('Open')); - await waitFor(() => expect(getByText('Resource')).toBeInTheDocument()); + await waitFor(() => + expect(screen.getByText('Resource')).toBeInTheDocument(), + ); - await userEvent.click(getByText('Resource')); + await userEvent.click(screen.getByText('Resource')); await waitFor(() => { expect(onChange).toHaveBeenCalledWith(undefined); @@ -102,13 +105,13 @@ describe('', () => { it('should return all values when cleared', async () => { const onChange = jest.fn(); - const { getByRole } = await renderWithEffects( + await renderWithEffects( , ); - await userEvent.click(getByRole('combobox')); + await userEvent.click(screen.getByRole('combobox')); await userEvent.tab(); await waitFor(() => { diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx index 316f83da62..0817a01e0b 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx @@ -30,11 +30,14 @@ import { Autocomplete } from '@material-ui/lab'; import React, { useCallback, useEffect, useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; -const useStyles = makeStyles({ - formControl: { - maxWidth: 300, +const useStyles = makeStyles( + { + formControl: { + maxWidth: 300, + }, }, -}); + { name: 'PluginCatalogGraphSelectedKindsFilter' }, +); export type Props = { value: string[] | undefined; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx index bcc67edc3d..8baa7865b9 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { RELATION_CHILD_OF, RELATION_HAS_MEMBER, RELATION_OWNED_BY, } from '@backstage/catalog-model'; -import { render, waitFor } from '@testing-library/react'; +import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { ALL_RELATION_PAIRS } from '../EntityRelationsGraph'; @@ -26,7 +27,7 @@ import { SelectedRelationsFilter } from './SelectedRelationsFilter'; describe('', () => { test('should render current value', () => { - const { getByText } = render( + render( ', () => { />, ); - expect(getByText(RELATION_OWNED_BY)).toBeInTheDocument(); - expect(getByText(RELATION_CHILD_OF)).toBeInTheDocument(); + expect(screen.getByText(RELATION_OWNED_BY)).toBeInTheDocument(); + expect(screen.getByText(RELATION_CHILD_OF)).toBeInTheDocument(); }); test('should select value', async () => { const onChange = jest.fn(); - const { getByText, getByLabelText } = render( + render( ', () => { />, ); - await userEvent.click(getByLabelText('Open')); + await userEvent.click(screen.getByLabelText('Open')); await waitFor(() => - expect(getByText(RELATION_HAS_MEMBER)).toBeInTheDocument(), + expect(screen.getByText(RELATION_HAS_MEMBER)).toBeInTheDocument(), ); - await userEvent.click(getByText(RELATION_HAS_MEMBER)); + await userEvent.click(screen.getByText(RELATION_HAS_MEMBER)); await waitFor(() => { expect(onChange).toHaveBeenCalledWith([ @@ -67,7 +68,7 @@ describe('', () => { test('should return undefined if all values are selected', async () => { const onChange = jest.fn(); - const { getByText, getByLabelText } = render( + render( p).filter( @@ -77,13 +78,13 @@ describe('', () => { />, ); - await userEvent.click(getByLabelText('Open')); + await userEvent.click(screen.getByLabelText('Open')); await waitFor(() => - expect(getByText(RELATION_HAS_MEMBER)).toBeInTheDocument(), + expect(screen.getByText(RELATION_HAS_MEMBER)).toBeInTheDocument(), ); - await userEvent.click(getByText(RELATION_HAS_MEMBER)); + await userEvent.click(screen.getByText(RELATION_HAS_MEMBER)); await waitFor(() => { expect(onChange).toHaveBeenCalledWith(undefined); @@ -92,7 +93,7 @@ describe('', () => { test('should return all values when cleared', async () => { const onChange = jest.fn(); - const { getByRole } = render( + render( ', () => { />, ); - await userEvent.click(getByRole('combobox')); + await userEvent.click(screen.getByRole('combobox')); await userEvent.tab(); await waitFor(() => { diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx index 839bfbe425..197f65cef8 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.tsx @@ -28,11 +28,14 @@ import { Autocomplete } from '@material-ui/lab'; import React, { useCallback, useMemo } from 'react'; import { RelationPairs } from '../EntityRelationsGraph'; -const useStyles = makeStyles({ - formControl: { - maxWidth: 300, +const useStyles = makeStyles( + { + formControl: { + maxWidth: 300, + }, }, -}); + { name: 'PluginCatalogGraphSelectedRelationsFilter' }, +); export type Props = { relationPairs: RelationPairs; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SwitchFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SwitchFilter.test.tsx index 08c7304989..322232d774 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SwitchFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SwitchFilter.test.tsx @@ -13,31 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { render } from '@testing-library/react'; + +import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { SwitchFilter } from './SwitchFilter'; describe('', () => { test('should render value', () => { - const { getByLabelText } = render( - {}} />, - ); + render( {}} />); - expect(getByLabelText('My label')).toBeInTheDocument(); - expect(getByLabelText('My label')).not.toBeChecked(); + expect(screen.getByLabelText('My label')).toBeInTheDocument(); + expect(screen.getByLabelText('My label')).not.toBeChecked(); }); test('should toggle value', async () => { const onChange = jest.fn(); - const { getByLabelText } = render( - , - ); + render(); - expect(getByLabelText('My label')).toBeInTheDocument(); - expect(getByLabelText('My label')).toBeChecked(); + expect(screen.getByLabelText('My label')).toBeInTheDocument(); + expect(screen.getByLabelText('My label')).toBeChecked(); - await userEvent.click(getByLabelText('My label')); + await userEvent.click(screen.getByLabelText('My label')); expect(onChange).toHaveBeenCalledWith(false); }); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SwitchFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SwitchFilter.tsx index 214080db6e..495f6b4db2 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SwitchFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SwitchFilter.tsx @@ -22,12 +22,15 @@ export type Props = { onChange: (value: boolean) => void; }; -const useStyles = makeStyles({ - root: { - width: '100%', - maxWidth: 300, +const useStyles = makeStyles( + { + root: { + width: '100%', + maxWidth: 300, + }, }, -}); + { name: 'PluginCatalogGraphSwitchFilter' }, +); export const SwitchFilter = ({ label, value, onChange }: Props) => { const classes = useStyles(); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.test.tsx index 5b8f50b983..ab280aea81 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.test.tsx @@ -13,17 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { RELATION_CHILD_OF, RELATION_PARENT_OF, } from '@backstage/catalog-model'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import React from 'react'; import { CustomLabel } from './CustomLabel'; describe('', () => { test('renders label', () => { - const { getByText } = render( + render( ', () => { , ); - expect(getByText(RELATION_PARENT_OF)).toBeInTheDocument(); + expect(screen.getByText(RELATION_PARENT_OF)).toBeInTheDocument(); }); test('renders label with multiple relations', () => { - const { getByText } = render( + render( ', () => { , ); - expect(getByText(RELATION_PARENT_OF)).toBeInTheDocument(); - expect(getByText(RELATION_CHILD_OF)).toBeInTheDocument(); + expect(screen.getByText(RELATION_PARENT_OF)).toBeInTheDocument(); + expect(screen.getByText(RELATION_CHILD_OF)).toBeInTheDocument(); }); }); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.tsx index 60a858eee6..19082cfe81 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomLabel.tsx @@ -20,14 +20,17 @@ import React from 'react'; import { EntityEdgeData } from './types'; import classNames from 'classnames'; -const useStyles = makeStyles((theme: BackstageTheme) => ({ - text: { - fill: theme.palette.textContrast, - }, - secondary: { - fill: theme.palette.textSubtle, - }, -})); +const useStyles = makeStyles( + (theme: BackstageTheme) => ({ + text: { + fill: theme.palette.textContrast, + }, + secondary: { + fill: theme.palette.textSubtle, + }, + }), + { name: 'PluginCatalogGraphCustomLabel' }, +); export function CustomLabel({ edge: { relations }, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.test.tsx index ff9fc9377e..7a34bbf688 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.test.tsx @@ -13,14 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { CustomNode } from './CustomNode'; import userEvent from '@testing-library/user-event'; describe('', () => { test('renders node', async () => { - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { , ); - expect(getByText('kind:namespace/name')).toBeInTheDocument(); + expect(screen.getByText('kind:namespace/name')).toBeInTheDocument(); }); test('renders node, skips default namespace', async () => { - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { , ); - expect(getByText('kind:name')).toBeInTheDocument(); + expect(screen.getByText('kind:name')).toBeInTheDocument(); }); test('renders node with onClick', async () => { const onClick = jest.fn(); - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { , ); - expect(getByText('kind:namespace/name')).toBeInTheDocument(); - await userEvent.click(getByText('kind:namespace/name')); + expect(screen.getByText('kind:namespace/name')).toBeInTheDocument(); + await userEvent.click(screen.getByText('kind:namespace/name')); expect(onClick).toHaveBeenCalledTimes(1); }); test('renders title if entity has one', async () => { - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { , ); - expect(getByText('Custom Title')).toBeInTheDocument(); + expect(screen.getByText('Custom Title')).toBeInTheDocument(); }); }); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx index ae541fa997..edcdb97fcb 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/CustomNode.tsx @@ -22,37 +22,40 @@ import React, { useLayoutEffect, useRef, useState } from 'react'; import { EntityKindIcon } from './EntityKindIcon'; import { EntityNodeData } from './types'; -const useStyles = makeStyles((theme: BackstageTheme) => ({ - node: { - fill: theme.palette.grey[300], - stroke: theme.palette.grey[300], +const useStyles = makeStyles( + (theme: BackstageTheme) => ({ + node: { + fill: theme.palette.grey[300], + stroke: theme.palette.grey[300], - '&.primary': { - fill: theme.palette.primary.light, - stroke: theme.palette.primary.light, + '&.primary': { + fill: theme.palette.primary.light, + stroke: theme.palette.primary.light, + }, + '&.secondary': { + fill: theme.palette.secondary.light, + stroke: theme.palette.secondary.light, + }, }, - '&.secondary': { - fill: theme.palette.secondary.light, - stroke: theme.palette.secondary.light, - }, - }, - text: { - fill: theme.palette.getContrastText(theme.palette.grey[300]), + text: { + fill: theme.palette.getContrastText(theme.palette.grey[300]), - '&.primary': { - fill: theme.palette.primary.contrastText, + '&.primary': { + fill: theme.palette.primary.contrastText, + }, + '&.secondary': { + fill: theme.palette.secondary.contrastText, + }, + '&.focused': { + fontWeight: 'bold', + }, }, - '&.secondary': { - fill: theme.palette.secondary.contrastText, + clickable: { + cursor: 'pointer', }, - '&.focused': { - fontWeight: 'bold', - }, - }, - clickable: { - cursor: 'pointer', - }, -})); + }), + { name: 'PluginCatalogGraphCustomNode' }, +); export function CustomNode({ node: { diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx index 5d5f9dbf3c..6592c6195e 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { EntityKindIcon } from './EntityKindIcon'; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx index c79f975789..8a3352abb4 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx @@ -22,106 +22,105 @@ import { RELATION_PART_OF, } from '@backstage/catalog-model'; import { DependencyGraphTypes } from '@backstage/core-components'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React, { FunctionComponent } from 'react'; import { EntityRelationsGraph } from './EntityRelationsGraph'; describe('', () => { let Wrapper: FunctionComponent; - let catalog: jest.Mocked; + const entities: { [ref: string]: Entity } = { + 'b:d/c': { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + namespace: 'd', + }, + relations: [ + { + targetRef: 'k:d/a1', + type: RELATION_OWNER_OF, + }, + { + targetRef: 'b:d/c1', + type: RELATION_HAS_PART, + }, + ], + }, + 'k:d/a1': { + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'a1', + namespace: 'd', + }, + relations: [ + { + targetRef: 'b:d/c', + type: RELATION_OWNED_BY, + }, + { + targetRef: 'b:d/c1', + type: RELATION_OWNED_BY, + }, + ], + }, + 'b:d/c1': { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c1', + namespace: 'd', + }, + relations: [ + { + targetRef: 'b:d/c', + type: RELATION_PART_OF, + }, + { + targetRef: 'k:d/a1', + type: RELATION_OWNER_OF, + }, + { + targetRef: 'b:d/c2', + type: RELATION_HAS_PART, + }, + ], + }, + 'b:d/c2': { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c2', + namespace: 'd', + }, + relations: [ + { + targetRef: 'b:d/c1', + type: RELATION_PART_OF, + }, + ], + }, + }; + const catalog = { + getEntities: jest.fn(), + getEntityByRef: jest.fn(), + removeEntityByUid: jest.fn(), + getLocationById: jest.fn(), + getLocationByRef: jest.fn(), + addLocation: jest.fn(), + removeLocationById: jest.fn(), + refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), + validateEntity: jest.fn(), + }; const CUSTOM_TEST_ID = 'custom-test-id'; beforeEach(() => { - const entities: { [ref: string]: Entity } = { - 'b:d/c': { - apiVersion: 'a', - kind: 'b', - metadata: { - name: 'c', - namespace: 'd', - }, - relations: [ - { - targetRef: 'k:d/a1', - type: RELATION_OWNER_OF, - }, - { - targetRef: 'b:d/c1', - type: RELATION_HAS_PART, - }, - ], - }, - 'k:d/a1': { - apiVersion: 'a', - kind: 'k', - metadata: { - name: 'a1', - namespace: 'd', - }, - relations: [ - { - targetRef: 'b:d/c', - type: RELATION_OWNED_BY, - }, - { - targetRef: 'b:d/c1', - type: RELATION_OWNED_BY, - }, - ], - }, - 'b:d/c1': { - apiVersion: 'a', - kind: 'b', - metadata: { - name: 'c1', - namespace: 'd', - }, - relations: [ - { - targetRef: 'b:d/c', - type: RELATION_PART_OF, - }, - { - targetRef: 'k:d/a1', - type: RELATION_OWNER_OF, - }, - { - targetRef: 'b:d/c2', - type: RELATION_HAS_PART, - }, - ], - }, - 'b:d/c2': { - apiVersion: 'a', - kind: 'b', - metadata: { - name: 'c2', - namespace: 'd', - }, - relations: [ - { - targetRef: 'b:d/c1', - type: RELATION_PART_OF, - }, - ], - }, - }; - catalog = { - getEntities: jest.fn(), - getEntityByRef: jest.fn(async n => entities[n as string]), - removeEntityByUid: jest.fn(), - getLocationById: jest.fn(), - getLocationByRef: jest.fn(), - addLocation: jest.fn(), - removeLocationById: jest.fn(), - refreshEntity: jest.fn(), - getEntityAncestors: jest.fn(), - getEntityFacets: jest.fn(), - validateEntity: jest.fn(), - }; - Wrapper = ({ children }) => ( {children} @@ -129,7 +128,7 @@ describe('', () => { ); }); - afterAll(() => { + afterEach(() => { jest.resetAllMocks(); }); @@ -144,7 +143,7 @@ describe('', () => { relations: [], }); - const { findByText, findAllByTestId } = await renderInTestApp( + await renderInTestApp( ', () => { , ); - expect(await findByText('b:d/c')).toBeInTheDocument(); - expect(await findAllByTestId('node')).toHaveLength(1); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + expect(await screen.findAllByTestId('node')).toHaveLength(1); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); }); test('renders a progress indicator while loading', async () => { catalog.getEntityByRef.mockImplementation(() => new Promise(() => {})); - const { findByRole } = await renderInTestApp( + await renderInTestApp( ', () => { , ); - expect(await findByRole('progressbar')).toBeInTheDocument(); + expect(await screen.findByRole('progressbar')).toBeInTheDocument(); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1); }); @@ -199,7 +198,7 @@ describe('', () => { return undefined; }); - const { findByText, findAllByTestId } = await renderInTestApp( + await renderInTestApp( ', () => { , ); - expect(await findByText('b:d/c')).toBeInTheDocument(); - expect(await findAllByTestId('node')).toHaveLength(1); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + expect(await screen.findAllByTestId('node')).toHaveLength(1); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(2); }); test('renders at max depth of one', async () => { - const { findByText, findAllByTestId, findAllByText } = - await renderInTestApp( - - - , - ); + catalog.getEntityByRef.mockImplementation(async n => entities[n as string]); - expect(await findByText('b:d/c')).toBeInTheDocument(); - expect(await findByText('b:d/c1')).toBeInTheDocument(); - expect(await findByText('k:d/a1')).toBeInTheDocument(); - expect(await findAllByTestId('node')).toHaveLength(3); + await renderInTestApp( + + + , + ); - expect(await findAllByText('ownerOf')).toHaveLength(1); - expect(await findAllByText('hasPart')).toHaveLength(1); - expect(await findAllByTestId('label')).toHaveLength(2); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + expect(await screen.findByText('b:d/c1')).toBeInTheDocument(); + expect(await screen.findByText('k:d/a1')).toBeInTheDocument(); + expect(await screen.findAllByTestId('node')).toHaveLength(3); + + expect(await screen.findAllByText('ownerOf')).toHaveLength(1); + expect(await screen.findAllByText('hasPart')).toHaveLength(1); + expect(await screen.findAllByTestId('label')).toHaveLength(2); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(3); }); - test('renders simplied graph at full depth', async () => { - const { findByText, findAllByText, findAllByTestId } = - await renderInTestApp( - - - , - ); + test('renders simplified graph at full depth', async () => { + catalog.getEntityByRef.mockImplementation(async n => entities[n as string]); - expect(await findByText('b:d/c')).toBeInTheDocument(); - expect(await findByText('b:d/c1')).toBeInTheDocument(); - expect(await findByText('k:d/a1')).toBeInTheDocument(); - expect(await findByText('b:d/c2')).toBeInTheDocument(); - expect(await findAllByTestId('node')).toHaveLength(4); + await renderInTestApp( + + + , + ); - expect(await findAllByText('ownerOf')).toHaveLength(1); - expect(await findAllByText('hasPart')).toHaveLength(2); - expect(await findAllByTestId('label')).toHaveLength(3); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + expect(await screen.findByText('b:d/c1')).toBeInTheDocument(); + expect(await screen.findByText('k:d/a1')).toBeInTheDocument(); + expect(await screen.findByText('b:d/c2')).toBeInTheDocument(); + expect(await screen.findAllByTestId('node')).toHaveLength(4); + + expect(await screen.findAllByText('ownerOf')).toHaveLength(1); + expect(await screen.findAllByText('hasPart')).toHaveLength(2); + expect(await screen.findAllByTestId('label')).toHaveLength(3); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(4); }); test('renders full graph at full depth', async () => { - const { findAllByText, findByText, findAllByTestId } = - await renderInTestApp( - - - , - ); + catalog.getEntityByRef.mockImplementation(async n => entities[n as string]); - expect(await findByText('b:d/c')).toBeInTheDocument(); - expect(await findByText('b:d/c1')).toBeInTheDocument(); - expect(await findByText('k:d/a1')).toBeInTheDocument(); - expect(await findByText('b:d/c2')).toBeInTheDocument(); - expect(await findAllByTestId('node')).toHaveLength(4); + await renderInTestApp( + + + , + ); - expect(await findAllByText('ownerOf')).toHaveLength(2); - expect(await findAllByText('ownedBy')).toHaveLength(2); - expect(await findAllByText('hasPart')).toHaveLength(2); - expect(await findAllByText('partOf')).toHaveLength(2); - expect(await findAllByTestId('label')).toHaveLength(8); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + expect(await screen.findByText('b:d/c1')).toBeInTheDocument(); + expect(await screen.findByText('k:d/a1')).toBeInTheDocument(); + expect(await screen.findByText('b:d/c2')).toBeInTheDocument(); + expect(await screen.findAllByTestId('node')).toHaveLength(4); + + expect(await screen.findAllByText('ownerOf')).toHaveLength(2); + expect(await screen.findAllByText('ownedBy')).toHaveLength(2); + expect(await screen.findAllByText('hasPart')).toHaveLength(2); + expect(await screen.findAllByText('partOf')).toHaveLength(2); + expect(await screen.findAllByTestId('label')).toHaveLength(8); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(4); }); test('renders full graph at full depth with merged relations', async () => { - const { findAllByText, findByText, findAllByTestId } = - await renderInTestApp( - - - , - ); + catalog.getEntityByRef.mockImplementation(async n => entities[n as string]); - expect(await findByText('b:d/c')).toBeInTheDocument(); - expect(await findByText('b:d/c1')).toBeInTheDocument(); - expect(await findByText('k:d/a1')).toBeInTheDocument(); - expect(await findByText('b:d/c2')).toBeInTheDocument(); - expect(await findAllByTestId('node')).toHaveLength(4); + await renderInTestApp( + + + , + ); - expect(await findAllByText('ownerOf')).toHaveLength(2); - expect(await findAllByText('hasPart')).toHaveLength(2); - expect(await findAllByTestId('label')).toHaveLength(4); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + expect(await screen.findByText('b:d/c1')).toBeInTheDocument(); + expect(await screen.findByText('k:d/a1')).toBeInTheDocument(); + expect(await screen.findByText('b:d/c2')).toBeInTheDocument(); + expect(await screen.findAllByTestId('node')).toHaveLength(4); + + expect(await screen.findAllByText('ownerOf')).toHaveLength(2); + expect(await screen.findAllByText('hasPart')).toHaveLength(2); + expect(await screen.findAllByTestId('label')).toHaveLength(4); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(4); }); test('renders a graph with multiple root nodes', async () => { - const { findAllByText, findByText, findAllByTestId } = - await renderInTestApp( - - - , - ); + catalog.getEntityByRef.mockImplementation(async n => entities[n as string]); - expect(await findByText('b:d/c')).toBeInTheDocument(); - expect(await findByText('b:d/c1')).toBeInTheDocument(); - expect(await findByText('k:d/a1')).toBeInTheDocument(); - expect(await findByText('b:d/c2')).toBeInTheDocument(); - expect(await findAllByTestId('node')).toHaveLength(4); + await renderInTestApp( + + + , + ); - expect(await findAllByText('ownerOf')).toHaveLength(1); - expect(await findAllByText('partOf')).toHaveLength(2); - expect(await findAllByTestId('label')).toHaveLength(3); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + expect(await screen.findByText('b:d/c1')).toBeInTheDocument(); + expect(await screen.findByText('k:d/a1')).toBeInTheDocument(); + expect(await screen.findByText('b:d/c2')).toBeInTheDocument(); + expect(await screen.findAllByTestId('node')).toHaveLength(4); + + expect(await screen.findAllByText('ownerOf')).toHaveLength(1); + expect(await screen.findAllByText('partOf')).toHaveLength(2); + expect(await screen.findAllByTestId('label')).toHaveLength(3); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(4); }); test('renders a graph with filtered kinds and relations', async () => { - const { findAllByText, findByText, findAllByTestId } = - await renderInTestApp( - - - , - ); + catalog.getEntityByRef.mockImplementation(async n => entities[n as string]); - expect(await findByText('b:d/c')).toBeInTheDocument(); - expect(await findByText('k:d/a1')).toBeInTheDocument(); - expect(await findAllByTestId('node')).toHaveLength(2); + await renderInTestApp( + + + , + ); - expect(await findAllByText('ownerOf')).toHaveLength(1); - expect(await findAllByTestId('label')).toHaveLength(1); + expect(await screen.findByText('b:d/c')).toBeInTheDocument(); + expect(await screen.findByText('k:d/a1')).toBeInTheDocument(); + expect(await screen.findAllByTestId('node')).toHaveLength(2); + + expect(await screen.findAllByText('ownerOf')).toHaveLength(1); + expect(await screen.findAllByTestId('label')).toHaveLength(1); expect(catalog.getEntityByRef).toHaveBeenCalledTimes(2); }); test('handle clicks on a node', async () => { + catalog.getEntityByRef.mockImplementation(async n => entities[n as string]); + const onNodeClick = jest.fn(); - const { findByText } = await renderInTestApp( + await renderInTestApp( ', () => { , ); - await userEvent.click(await findByText('k:d/a1')); + await userEvent.click(await screen.findByText('k:d/a1')); expect(onNodeClick).toHaveBeenCalledTimes(1); }); test('render custom node', async () => { + catalog.getEntityByRef.mockImplementation(async n => entities[n as string]); + const renderNode = (props: DependencyGraphTypes.RenderNodeProps) => ( {props.node.id} @@ -383,7 +392,7 @@ describe('', () => { ); - const { findAllByTestId, container } = await renderInTestApp( + const { container } = await renderInTestApp( ', () => { , ); - const node = await findAllByTestId(CUSTOM_TEST_ID); + const node = await screen.findAllByTestId(CUSTOM_TEST_ID); expect(node[0]).toBeInTheDocument(); expect(container.querySelector('circle')).toBeInTheDocument(); }); test('render custom label', async () => { + catalog.getEntityByRef.mockImplementation(async n => entities[n as string]); + const renderLabel = (props: DependencyGraphTypes.RenderLabelProps) => ( {`Test-Label${props.edge.label}`} @@ -405,7 +416,7 @@ describe('', () => { ); - const { findAllByTestId, findAllByText, container } = await renderInTestApp( + const { container } = await renderInTestApp( ', () => { /> , ); - const node = await findAllByTestId(CUSTOM_TEST_ID); + const node = await screen.findAllByTestId(CUSTOM_TEST_ID); expect(node[0]).toBeInTheDocument(); expect(container.querySelector('circle')).toBeInTheDocument(); - const labels = await findAllByText('Test-Labelvisible'); + const labels = await screen.findAllByText('Test-Labelvisible'); expect(labels[0]).toBeInTheDocument(); }); }); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index c0e2f610c1..67aa230ce2 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -32,36 +32,39 @@ import { ALL_RELATION_PAIRS, RelationPairs } from './relations'; import { Direction, EntityEdge, EntityNode } from './types'; import { useEntityRelationNodesAndEdges } from './useEntityRelationNodesAndEdges'; -const useStyles = makeStyles(theme => ({ - progress: { - position: 'absolute', - left: '50%', - top: '50%', - marginLeft: '-20px', - marginTop: '-20px', - }, - container: { - position: 'relative', - width: '100%', - display: 'flex', - flexDirection: 'column', - }, - graph: { - width: '100%', - flex: 1, - // Right now there is no good way to style edges between nodes, we have to - // fallback to these hacks: - '& path[marker-end]': { - transition: 'filter 0.1s ease-in-out', +const useStyles = makeStyles( + theme => ({ + progress: { + position: 'absolute', + left: '50%', + top: '50%', + marginLeft: '-20px', + marginTop: '-20px', }, - '& path[marker-end]:hover': { - filter: `drop-shadow(2px 2px 4px ${theme.palette.primary.dark});`, + container: { + position: 'relative', + width: '100%', + display: 'flex', + flexDirection: 'column', }, - '& g[data-testid=label]': { - transition: 'transform 0s', + graph: { + width: '100%', + flex: 1, + // Right now there is no good way to style edges between nodes, we have to + // fallback to these hacks: + '& path[marker-end]': { + transition: 'filter 0.1s ease-in-out', + }, + '& path[marker-end]:hover': { + filter: `drop-shadow(2px 2px 4px ${theme.palette.primary.dark});`, + }, + '& g[data-testid=label]': { + transition: 'transform 0s', + }, }, - }, -})); + }), + { name: 'PluginCatalogGraphEntityRelationsGraph' }, +); /** * Core building block for custom entity relations diagrams. @@ -86,7 +89,7 @@ export const EntityRelationsGraph = (props: { }) => { const { rootEntityNames, - maxDepth = Number.POSITIVE_INFINITY, + maxDepth = 2, unidirectional = true, mergeRelations = true, kinds, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts index e76b1001c6..802f993802 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity } from '@backstage/catalog-model'; import { useApi as useApiMocked } from '@backstage/core-plugin-api'; -import { CatalogApi } from '@backstage/plugin-catalog-react'; import { act, renderHook } from '@testing-library/react-hooks'; import { useEntityStore } from './useEntityStore'; @@ -24,23 +24,21 @@ jest.mock('@backstage/core-plugin-api'); const useApi = useApiMocked as jest.Mocked; describe('useEntityStore', () => { - let catalogApi: jest.Mocked; + const catalogApi = { + getEntities: jest.fn(), + getEntityByRef: jest.fn(), + removeEntityByUid: jest.fn(), + getLocationById: jest.fn(), + getLocationByRef: jest.fn(), + addLocation: jest.fn(), + removeLocationById: jest.fn(), + refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), + getEntityFacets: jest.fn(), + validateEntity: jest.fn(), + }; beforeEach(() => { - catalogApi = { - getEntities: jest.fn(), - getEntityByRef: jest.fn(), - removeEntityByUid: jest.fn(), - getLocationById: jest.fn(), - getLocationByRef: jest.fn(), - addLocation: jest.fn(), - removeLocationById: jest.fn(), - refreshEntity: jest.fn(), - getEntityAncestors: jest.fn(), - getEntityFacets: jest.fn(), - validateEntity: jest.fn(), - }; - useApi.mockReturnValue(catalogApi); }); diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 97b2a49835..0ea129e544 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-catalog-graphql +## 0.3.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## 0.3.16-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + ## 0.3.15-next.0 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 503c7eb3b0..ea2348d7cc 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-graphql", "description": "An experimental Backstage catalog GraphQL module", - "version": "0.3.15-next.0", + "version": "0.3.16-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -53,7 +53,7 @@ "@graphql-codegen/typescript": "^2.4.2", "@graphql-codegen/typescript-resolvers": "^2.4.3", "@graphql-tools/schema": "^9.0.0", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/catalog-import/.eslintrc.js b/plugins/catalog-import/.eslintrc.js index e2a53a6ad2..45bb6db521 100644 --- a/plugins/catalog-import/.eslintrc.js +++ b/plugins/catalog-import/.eslintrc.js @@ -1 +1,5 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'testing-library/prefer-screen-queries': 'error', + }, +}); diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 8562b77510..b161ac4110 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-catalog-import +## 0.9.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + +## 0.9.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + +## 0.9.1 + +### Patch Changes + +- 1e7b640518: Get rid of `this-is-undefined-in-esm` warning +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/integration-react@1.1.6 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-catalog-common@1.0.8 + +## 0.9.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.9.1-next.0 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index b8cacc4450..89de554079 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.9.1-next.0", + "version": "0.9.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -68,7 +68,7 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index d507ac3bef..e77abc8a85 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -49,7 +49,7 @@ jest.mock('@octokit/rest', () => { import { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; import { ScmAuthApi } from '@backstage/integration-react'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { CatalogApi } from '@backstage/plugin-catalog-react'; import { setupRequestMockHandlers } from '@backstage/test-utils'; import { Octokit } from '@octokit/rest'; import { rest } from 'msw'; @@ -89,7 +89,7 @@ describe('CatalogImportClient', () => { }), ); - const catalogApi: jest.Mocked = { + const catalogApi = { getEntities: jest.fn(), addLocation: jest.fn(), removeLocationById: jest.fn(), @@ -111,7 +111,7 @@ describe('CatalogImportClient', () => { scmAuthApi, scmIntegrationsApi, identityApi, - catalogApi, + catalogApi: catalogApi as Partial as CatalogApi, configApi: new ConfigReader({ app: { baseUrl: 'https://demo.backstage.io/', @@ -458,7 +458,7 @@ describe('CatalogImportClient', () => { scmAuthApi, scmIntegrationsApi, identityApi, - catalogApi, + catalogApi: catalogApi as Partial as CatalogApi, configApi: new ConfigReader({ catalog: { import: { @@ -610,7 +610,7 @@ describe('CatalogImportClient', () => { scmAuthApi, scmIntegrationsApi, identityApi, - catalogApi, + catalogApi: catalogApi as Partial as CatalogApi, configApi: new ConfigReader({ catalog: { import: { @@ -680,7 +680,7 @@ describe('CatalogImportClient', () => { scmAuthApi, scmIntegrationsApi, identityApi, - catalogApi, + catalogApi: catalogApi as Partial as CatalogApi, configApi: new ConfigReader({ catalog: { import: { diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx index d6132b249e..a231563484 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx @@ -19,6 +19,7 @@ import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { configApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { catalogImportApiRef, CatalogImportClient } from '../../api'; import { DefaultImportPage } from './DefaultImportPage'; @@ -65,14 +66,14 @@ describe('', () => { }); it('renders without exploding', async () => { - const { getByText } = await renderInTestApp( + await renderInTestApp( , ); expect( - getByText('Start tracking your component in Backstage'), + screen.getByText('Start tracking your component in Backstage'), ).toBeInTheDocument(); }); }); diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx index 9a27532c57..c33de77ffd 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx @@ -21,6 +21,7 @@ import { TestApiProvider, TestApiRegistry, } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { CatalogImportApi, catalogImportApiRef } from '../../api'; import { ImportInfoCard } from './ImportInfoCard'; @@ -49,7 +50,7 @@ describe('', () => { }); it('renders without exploding', async () => { - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { , ); - expect(getByText('Register an existing component')).toBeInTheDocument(); + expect( + screen.getByText('Register an existing component'), + ).toBeInTheDocument(); }); it('renders section on GitHub discovery if supported', async () => { catalogImportApi.preparePullRequest = async () => ({ title: '', body: '' }); - const { getByText } = await renderInTestApp( + await renderInTestApp( , ); - expect(getByText(/The wizard discovers all/)).toBeInTheDocument(); + expect(screen.getByText(/The wizard discovers all/)).toBeInTheDocument(); }); it('renders section on pull requests if supported', async () => { catalogImportApi.preparePullRequest = async () => ({ title: '', body: '' }); - const { getByText } = await renderInTestApp( + await renderInTestApp( , ); expect( - getByText(/the wizard will prepare a Pull Request/), + screen.getByText(/the wizard will prepare a Pull Request/), ).toBeInTheDocument(); }); }); diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx index 498e786eb3..a38b801607 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx @@ -19,6 +19,7 @@ import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { configApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { useOutlet } from 'react-router'; import { catalogImportApiRef, CatalogImportClient } from '../../api'; @@ -71,26 +72,26 @@ describe('', () => { afterEach(() => jest.resetAllMocks()); it('renders without exploding', async () => { - const { getByText } = await renderInTestApp( + await renderInTestApp( , ); expect( - getByText('Start tracking your component in Backstage'), + screen.getByText('Start tracking your component in Backstage'), ).toBeInTheDocument(); }); it('renders with custom children', async () => { (useOutlet as jest.Mock).mockReturnValue(
Hello World
); - const { getByText } = await renderInTestApp( + await renderInTestApp( , ); - expect(getByText('Hello World')).toBeInTheDocument(); + expect(screen.getByText('Hello World')).toBeInTheDocument(); }); }); diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx index 5a8110eb6c..a9d662cab0 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx @@ -16,7 +16,7 @@ import { errorApiRef } from '@backstage/core-plugin-api'; import { TestApiProvider } from '@backstage/test-utils'; -import { act, render } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { AnalyzeResult, catalogImportApiRef } from '../../api/'; @@ -60,19 +60,20 @@ describe('', () => { }); it('renders without exploding', async () => { - const { getByRole } = render( - undefined} />, - { - wrapper: Wrapper, - }, - ); + render( undefined} />, { + wrapper: Wrapper, + }); - expect(getByRole('textbox', { name: /Repository/i })).toBeInTheDocument(); - expect(getByRole('textbox', { name: /Repository/i })).toHaveValue(''); + expect( + screen.getByRole('textbox', { name: /Repository/i }), + ).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /Repository/i })).toHaveValue( + '', + ); }); it('should use default analysis url', async () => { - const { getByRole } = render( + render( undefined} analysisUrl="https://default" @@ -82,8 +83,10 @@ describe('', () => { }, ); - expect(getByRole('textbox', { name: /Repository/i })).toBeInTheDocument(); - expect(getByRole('textbox', { name: /Repository/i })).toHaveValue( + expect( + screen.getByRole('textbox', { name: /Repository/i }), + ).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: /Repository/i })).toHaveValue( 'https://default', ); }); @@ -91,16 +94,13 @@ describe('', () => { it('should not analyze without url', async () => { const onAnalysisFn = jest.fn(); - const { getByRole } = render( - , - { - wrapper: Wrapper, - }, - ); + render(, { + wrapper: Wrapper, + }); await act(async () => { try { - await userEvent.click(getByRole('button', { name: /Analyze/i })); + await userEvent.click(screen.getByRole('button', { name: /Analyze/i })); } catch { return; } @@ -114,26 +114,23 @@ describe('', () => { it('should not analyze invalid value', async () => { const onAnalysisFn = jest.fn(); - const { getByRole, getByText } = render( - , - { - wrapper: Wrapper, - }, - ); + render(, { + wrapper: Wrapper, + }); await act(async () => { await userEvent.type( - getByRole('textbox', { name: /Repository/i }), + screen.getByRole('textbox', { name: /Repository/i }), 'http:/', ); - await userEvent.click(getByRole('button', { name: /Analyze/i })); + await userEvent.click(screen.getByRole('button', { name: /Analyze/i })); }); expect(catalogImportApi.analyzeUrl).toHaveBeenCalledTimes(0); expect(onAnalysisFn).toHaveBeenCalledTimes(0); expect(errorApi.post).toHaveBeenCalledTimes(0); expect( - getByText('Must start with http:// or https://.'), + screen.getByText('Must start with http:// or https://.'), ).toBeInTheDocument(); }); @@ -145,12 +142,9 @@ describe('', () => { locations: [location], } as AnalyzeResult; - const { getByRole } = render( - , - { - wrapper: Wrapper, - }, - ); + render(, { + wrapper: Wrapper, + }); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve(analyzeResult), @@ -158,10 +152,10 @@ describe('', () => { await act(async () => { await userEvent.type( - getByRole('textbox', { name: /Repository/i }), + screen.getByRole('textbox', { name: /Repository/i }), 'https://my-repository', ); - await userEvent.click(getByRole('button', { name: /Analyze/i })); + await userEvent.click(screen.getByRole('button', { name: /Analyze/i })); }); expect(onAnalysisFn).toHaveBeenCalledTimes(1); @@ -182,12 +176,9 @@ describe('', () => { locations: [location, location], } as AnalyzeResult; - const { getByRole } = render( - , - { - wrapper: Wrapper, - }, - ); + render(, { + wrapper: Wrapper, + }); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve(analyzeResult), @@ -195,10 +186,10 @@ describe('', () => { await act(async () => { await userEvent.type( - getByRole('textbox', { name: /Repository/i }), + screen.getByRole('textbox', { name: /Repository/i }), 'https://my-repository-1', ); - await userEvent.click(getByRole('button', { name: /Analyze/i })); + await userEvent.click(screen.getByRole('button', { name: /Analyze/i })); }); expect(onAnalysisFn).toHaveBeenCalledTimes(1); @@ -218,12 +209,9 @@ describe('', () => { locations: [], } as AnalyzeResult; - const { getByRole, getByText } = render( - , - { - wrapper: Wrapper, - }, - ); + render(, { + wrapper: Wrapper, + }); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve(analyzeResult), @@ -231,15 +219,15 @@ describe('', () => { await act(async () => { await userEvent.type( - getByRole('textbox', { name: /Repository/i }), + screen.getByRole('textbox', { name: /Repository/i }), 'https://my-repository-1', ); - await userEvent.click(getByRole('button', { name: /Analyze/i })); + await userEvent.click(screen.getByRole('button', { name: /Analyze/i })); }); expect(onAnalysisFn).toHaveBeenCalledTimes(0); expect( - getByText('There are no entities at this location'), + screen.getByText('There are no entities at this location'), ).toBeInTheDocument(); expect(errorApi.post).toHaveBeenCalledTimes(0); }); @@ -262,12 +250,9 @@ describe('', () => { ], } as AnalyzeResult; - const { getByRole } = render( - , - { - wrapper: Wrapper, - }, - ); + render(, { + wrapper: Wrapper, + }); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve(analyzeResult), @@ -275,10 +260,10 @@ describe('', () => { await act(async () => { await userEvent.type( - getByRole('textbox', { name: /Repository/i }), + screen.getByRole('textbox', { name: /Repository/i }), 'https://my-repository-2', ); - await userEvent.click(getByRole('button', { name: /Analyze/i })); + await userEvent.click(screen.getByRole('button', { name: /Analyze/i })); }); expect(onAnalysisFn).toHaveBeenCalledTimes(1); @@ -300,12 +285,9 @@ describe('', () => { generatedEntities: [], } as AnalyzeResult; - const { getByRole, getByText } = render( - , - { - wrapper: Wrapper, - }, - ); + render(, { + wrapper: Wrapper, + }); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve(analyzeResult), @@ -313,15 +295,15 @@ describe('', () => { await act(async () => { await userEvent.type( - getByRole('textbox', { name: /Repository/i }), + screen.getByRole('textbox', { name: /Repository/i }), 'https://my-repository-2', ); - await userEvent.click(getByRole('button', { name: /Analyze/i })); + await userEvent.click(screen.getByRole('button', { name: /Analyze/i })); }); expect(onAnalysisFn).toHaveBeenCalledTimes(0); expect( - getByText("Couldn't generate entities for your repository"), + screen.getByText("Couldn't generate entities for your repository"), ).toBeInTheDocument(); expect(errorApi.post).toHaveBeenCalledTimes(0); }); @@ -344,7 +326,7 @@ describe('', () => { ], } as AnalyzeResult; - const { getByRole, getByText } = render( + render( , { wrapper: Wrapper, @@ -357,15 +339,15 @@ describe('', () => { await act(async () => { await userEvent.type( - getByRole('textbox', { name: /Repository/i }), + screen.getByRole('textbox', { name: /Repository/i }), 'https://my-repository-2', ); - await userEvent.click(getByRole('button', { name: /Analyze/i })); + await userEvent.click(screen.getByRole('button', { name: /Analyze/i })); }); expect(onAnalysisFn).toHaveBeenCalledTimes(0); expect( - getByText("Couldn't generate entities for your repository"), + screen.getByText("Couldn't generate entities for your repository"), ).toBeInTheDocument(); expect(errorApi.post).toHaveBeenCalledTimes(0); }); @@ -373,12 +355,9 @@ describe('', () => { it('should report unknown type to the errorapi', async () => { const onAnalysisFn = jest.fn(); - const { getByRole, getByText } = render( - , - { - wrapper: Wrapper, - }, - ); + render(, { + wrapper: Wrapper, + }); catalogImportApi.analyzeUrl.mockReturnValueOnce( Promise.resolve({ type: 'unknown' } as any as AnalyzeResult), @@ -386,15 +365,15 @@ describe('', () => { await act(async () => { await userEvent.type( - getByRole('textbox', { name: /Repository/i }), + screen.getByRole('textbox', { name: /Repository/i }), 'https://my-repository-2', ); - await userEvent.click(getByRole('button', { name: /Analyze/i })); + await userEvent.click(screen.getByRole('button', { name: /Analyze/i })); }); expect(onAnalysisFn).toHaveBeenCalledTimes(0); expect( - getByText( + screen.getByText( 'Received unknown analysis result of type unknown. Please contact the support team.', ), ).toBeInTheDocument(); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx index 3acb7390a9..a8188ce358 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreparePullRequestForm.test.tsx @@ -15,7 +15,7 @@ */ import { FormHelperText, TextField } from '@material-ui/core'; -import { act, render } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { asInputRef } from '../helpers'; @@ -25,7 +25,7 @@ describe('', () => { it('renders without exploding', async () => { const onSubmitFn = jest.fn(); - const { getByRole } = render( + render( defaultValues={{ main: 'default' }} render={({ register }) => ( @@ -39,7 +39,7 @@ describe('', () => { ); await act(async () => { - await userEvent.click(getByRole('button', { name: /submit/i })); + await userEvent.click(screen.getByRole('button', { name: /submit/i })); }); expect(onSubmitFn).toHaveBeenCalledTimes(1); @@ -49,7 +49,7 @@ describe('', () => { it('should register a text field', async () => { const onSubmitFn = jest.fn(); - const { getByRole, getByLabelText } = render( + render( defaultValues={{ main: 'default' }} render={({ register }) => ( @@ -67,9 +67,9 @@ describe('', () => { ); await act(async () => { - await userEvent.clear(getByLabelText('Main Field')); - await userEvent.type(getByLabelText('Main Field'), 'My Text'); - await userEvent.click(getByRole('button', { name: /submit/i })); + await userEvent.clear(screen.getByLabelText('Main Field')); + await userEvent.type(screen.getByLabelText('Main Field'), 'My Text'); + await userEvent.click(screen.getByRole('button', { name: /submit/i })); }); expect(onSubmitFn).toHaveBeenCalledTimes(1); @@ -79,7 +79,7 @@ describe('', () => { it('registers required attribute', async () => { const onSubmitFn = jest.fn(); - const { queryByText, getByRole } = render( + render( defaultValues={{}} render={({ formState, register }) => ( @@ -100,13 +100,17 @@ describe('', () => { />, ); - expect(queryByText('Error in required main field')).not.toBeInTheDocument(); + expect( + screen.queryByText('Error in required main field'), + ).not.toBeInTheDocument(); await act(async () => { - await userEvent.click(getByRole('button', { name: /submit/i })); + await userEvent.click(screen.getByRole('button', { name: /submit/i })); }); expect(onSubmitFn).not.toHaveBeenCalled(); - expect(queryByText('Error in required main field')).toBeInTheDocument(); + expect( + screen.getByText('Error in required main field'), + ).toBeInTheDocument(); }); }); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx index 8d0d085b8a..d983b6d359 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.test.tsx @@ -15,7 +15,7 @@ */ import { makeStyles } from '@material-ui/core'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; @@ -28,15 +28,15 @@ const useStyles = makeStyles({ describe('', () => { it('renders without exploding', async () => { - const { getByText } = render( + render( , ); - const title = getByText('My Title'); - const description = getByText('description', { selector: 'strong' }); + const title = screen.getByText('My Title'); + const description = screen.getByText('description', { selector: 'strong' }); expect(title).toBeInTheDocument(); expect(title).toBeVisible(); expect(description).toBeInTheDocument(); @@ -46,7 +46,7 @@ describe('', () => { it('renders card with custom styles', async () => { const { result } = renderHook(() => useStyles()); - const { getByText } = render( + render( ', () => { />, ); - const title = getByText('My Title'); - const description = getByText('description', { selector: 'strong' }); + const title = screen.getByText('My Title'); + const description = screen.getByText('description', { selector: 'strong' }); expect(title).toBeInTheDocument(); expect(title).not.toBeVisible(); expect(description).toBeInTheDocument(); @@ -65,7 +65,7 @@ describe('', () => { it('renders with custom styles', async () => { const { result } = renderHook(() => useStyles()); - const { getByText } = render( + render( ', () => { />, ); - const title = getByText('My Title'); - const description = getByText('description', { selector: 'strong' }); + const title = screen.getByText('My Title'); + const description = screen.getByText('description', { selector: 'strong' }); expect(title).toBeInTheDocument(); expect(title).toBeVisible(); expect(description).toBeInTheDocument(); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index 5ad7e27c4b..36f1217c37 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -35,7 +35,7 @@ describe('', () => { preparePullRequest: jest.fn(), }; - const catalogApi: jest.Mocked = { + const catalogApi = { getEntities: jest.fn(), addLocation: jest.fn(), getEntityByRef: jest.fn(), @@ -102,7 +102,7 @@ describe('', () => { catalogApi.getEntities.mockReturnValue(Promise.resolve({ items: [] })); await act(async () => { - const { findByText } = render( + render( ', () => { }, ); - const title = await findByText('My title'); - const description = await findByText('body', { selector: 'strong' }); + const title = await screen.findByText('My title'); + const description = await screen.findByText('body', { + selector: 'strong', + }); expect(title).toBeInTheDocument(); expect(title).toBeVisible(); expect(description).toBeInTheDocument(); diff --git a/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx index 1817ece1b8..b5efd3fbbf 100644 --- a/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareSelectLocations/StepPrepareSelectLocations.test.tsx @@ -15,7 +15,7 @@ */ import { renderInTestApp } from '@backstage/test-utils'; -import { act } from '@testing-library/react'; +import { act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { AnalyzeResult } from '../../api'; @@ -58,7 +58,7 @@ describe('', () => { }); it('renders display locations to be added', async () => { - const rendered = await renderInTestApp( + await renderInTestApp( undefined} @@ -66,15 +66,15 @@ describe('', () => { />, ); - expect(rendered.getByText('url-1')).toBeInTheDocument(); - expect(rendered.getByText('url-2')).toBeInTheDocument(); + expect(screen.getByText('url-1')).toBeInTheDocument(); + expect(screen.getByText('url-2')).toBeInTheDocument(); expect( - rendered.queryByText(/Select one or more locations/), + screen.getByText(/Select one or more locations/), ).toBeInTheDocument(); expect( - rendered.queryByText(/locations already exist/), + screen.queryByText(/locations already exist/), ).not.toBeInTheDocument(); - expect(rendered.getByRole('button', { name: /Review/i })).toBeDisabled(); + expect(screen.getByRole('button', { name: /Review/i })).toBeDisabled(); }); it('should display existing locations only', async () => { @@ -95,7 +95,7 @@ describe('', () => { ], } as Extract; - const rendered = await renderInTestApp( + await renderInTestApp( undefined} @@ -103,15 +103,15 @@ describe('', () => { />, ); - expect(rendered.getByText(/my-target/)).toBeInTheDocument(); - expect(rendered.queryByText(/locations already exist/)).toBeInTheDocument(); + expect(screen.getByText(/my-target/)).toBeInTheDocument(); + expect(screen.getByText(/locations already exist/)).toBeInTheDocument(); expect( - rendered.queryByText(/Select one or more locations/), + screen.queryByText(/Select one or more locations/), ).not.toBeInTheDocument(); }); it('should select and deselect all', async () => { - const { getByRole, getAllByRole } = await renderInTestApp( + await renderInTestApp( undefined} @@ -119,27 +119,31 @@ describe('', () => { />, ); - const checkboxes = getAllByRole('checkbox'); + const checkboxes = screen.getAllByRole('checkbox'); checkboxes.forEach(c => expect(c).not.toBeChecked()); - expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + expect(screen.getByRole('button', { name: /Review/i })).toBeDisabled(); await act(async () => { - await userEvent.click(getByRole('button', { name: /Select All/i })); + await userEvent.click( + screen.getByRole('button', { name: /Select All/i }), + ); }); checkboxes.forEach(c => expect(c).toBeChecked()); - expect(getByRole('button', { name: /Review/i })).not.toBeDisabled(); + expect(screen.getByRole('button', { name: /Review/i })).not.toBeDisabled(); await act(async () => { - await userEvent.click(getByRole('button', { name: /Select All/i })); + await userEvent.click( + screen.getByRole('button', { name: /Select All/i }), + ); }); checkboxes.forEach(c => expect(c).not.toBeChecked()); - expect(getByRole('button', { name: /Review/i })).toBeDisabled(); + expect(screen.getByRole('button', { name: /Review/i })).toBeDisabled(); }); it('should preselect prepared locations', async () => { - const { getAllByRole } = await renderInTestApp( + await renderInTestApp( ', () => { />, ); - const checkboxes = getAllByRole('checkbox'); + const checkboxes = screen.getAllByRole('checkbox'); expect(checkboxes[0]).not.toBeChecked(); expect(checkboxes[1]).toBeChecked(); @@ -159,7 +163,7 @@ describe('', () => { }); it('should select items', async () => { - const { getAllByRole } = await renderInTestApp( + await renderInTestApp( undefined} @@ -167,7 +171,7 @@ describe('', () => { />, ); - const checkboxes = getAllByRole('checkbox'); + const checkboxes = screen.getAllByRole('checkbox'); checkboxes.forEach(c => expect(c).not.toBeChecked()); await act(async () => { @@ -188,7 +192,7 @@ describe('', () => { it('should go back', async () => { const onGoBack = jest.fn(); - const { getByRole } = await renderInTestApp( + await renderInTestApp( undefined} @@ -197,7 +201,7 @@ describe('', () => { ); await act(async () => { - await userEvent.click(getByRole('button', { name: /Back/i })); + await userEvent.click(screen.getByRole('button', { name: /Back/i })); }); expect(onGoBack).toHaveBeenCalledTimes(1); @@ -206,7 +210,7 @@ describe('', () => { it('should submit', async () => { const onPrepare = jest.fn(); - const { getAllByRole, getByRole } = await renderInTestApp( + await renderInTestApp( ', () => { />, ); - const checkboxes = getAllByRole('checkbox'); + const checkboxes = screen.getAllByRole('checkbox'); await act(async () => { await userEvent.click(checkboxes[1]); }); await act(async () => { - await userEvent.click(getByRole('button', { name: /Review/i })); + await userEvent.click(screen.getByRole('button', { name: /Review/i })); }); expect(onPrepare).toHaveBeenCalledTimes(1); diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index ffbd18cd8e..98f31a7498 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-catalog-node +## 1.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + +## 1.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + +## 1.2.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/catalog-client@1.1.2 + - @backstage/errors@1.1.3 + - @backstage/plugin-catalog-common@1.0.8 + +## 1.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-catalog-common@1.0.8-next.0 + ## 1.2.1-next.0 ### Patch Changes diff --git a/plugins/catalog-node/api-report.md b/plugins/catalog-node/api-report.md index 09a05ec7d9..9e9045db84 100644 --- a/plugins/catalog-node/api-report.md +++ b/plugins/catalog-node/api-report.md @@ -139,10 +139,16 @@ export type EntityProviderMutation = | { type: 'delta'; added: DeferredEntity[]; - removed: DeferredEntity[]; + removed: ( + | DeferredEntity + | { + entityRef: string; + locationKey?: string; + } + )[]; }; -// @public (undocumented) +// @public export type EntityProviderRefreshOptions = { keys: string[]; }; diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 320f372249..57440c4fa7 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-node", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", - "version": "1.2.1-next.0", + "version": "1.2.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-node/src/api/provider.ts b/plugins/catalog-node/src/api/provider.ts index fc9a172e0d..a576f32022 100644 --- a/plugins/catalog-node/src/api/provider.ts +++ b/plugins/catalog-node/src/api/provider.ts @@ -17,16 +17,26 @@ import { DeferredEntity } from '../processing'; /** - * @public * A 'full' mutation replaces all existing entities created by this entity provider with new ones. * A 'delta' mutation can both add and remove entities provided by this provider. Previously provided * entities from a 'full' mutation are not removed. + * + * @public */ export type EntityProviderMutation = - | { type: 'full'; entities: DeferredEntity[] } - | { type: 'delta'; added: DeferredEntity[]; removed: DeferredEntity[] }; + | { + type: 'full'; + entities: DeferredEntity[]; + } + | { + type: 'delta'; + added: DeferredEntity[]; + removed: (DeferredEntity | { entityRef: string; locationKey?: string })[]; + }; /** + * The options given to an entity refresh operation. + * * @public */ export type EntityProviderRefreshOptions = { @@ -34,30 +44,38 @@ export type EntityProviderRefreshOptions = { }; /** - * The EntityProviderConnection is the connection between the catalog and the entity provider. - * The EntityProvider use this connection to add and remove entities from the catalog. + * The connection between the catalog and the entity provider. + * Entity providers use this connection to add and remove entities from the catalog. + * * @public */ export interface EntityProviderConnection { /** - * Applies either a full or delta update to the catalog engine. + * Applies either a full or a delta update to the catalog engine. */ applyMutation(mutation: EntityProviderMutation): Promise; /** - * Schedules a refresh on all of the entities that has a matching refresh key associated with the provided keys. + * Schedules a refresh on all of the entities that have a matching refresh key associated with the provided keys. */ refresh(options: EntityProviderRefreshOptions): Promise; } /** - * An EntityProvider is able to provide entities to the catalog. + * An entity provider is able to provide entities to the catalog. * See https://backstage.io/docs/features/software-catalog/life-of-an-entity for more details. + * * @public */ export interface EntityProvider { - /** Unique provider name used internally for caching. */ + /** + * The name of the provider, which must be unique for all providers that are + * active in a catalog, and stable over time since emitted entities are + * related to the provider by this name. + */ getProviderName(): string; - /** Connect is called upon initialization by the catalog engine. */ + /** + * Called upon initialization by the catalog engine. + */ connect(connection: EntityProviderConnection): Promise; } diff --git a/plugins/catalog-node/src/catalogService.test.ts b/plugins/catalog-node/src/catalogService.test.ts index 9ecf08b30e..593f5bd59c 100644 --- a/plugins/catalog-node/src/catalogService.test.ts +++ b/plugins/catalog-node/src/catalogService.test.ts @@ -18,7 +18,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { createBackendModule, createServiceFactory, - discoveryServiceRef, + coreServices, } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; import { CatalogClient } from '@backstage/catalog-client'; @@ -29,7 +29,7 @@ describe('catalogServiceRef', () => { expect.assertions(1); const mockDiscoveryFactory = createServiceFactory({ - service: discoveryServiceRef, + service: coreServices.discovery, deps: {}, factory: async ({}) => { return async () => jest.fn() as unknown as PluginEndpointDiscovery; diff --git a/plugins/catalog-node/src/catalogService.ts b/plugins/catalog-node/src/catalogService.ts index c6a8fb44f5..0c54bd2d9d 100644 --- a/plugins/catalog-node/src/catalogService.ts +++ b/plugins/catalog-node/src/catalogService.ts @@ -17,7 +17,7 @@ import { createServiceFactory, createServiceRef, - discoveryServiceRef, + coreServices, } from '@backstage/backend-plugin-api'; import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; @@ -31,7 +31,7 @@ export const catalogServiceRef = createServiceRef({ createServiceFactory({ service, deps: { - discoveryApi: discoveryServiceRef, + discoveryApi: coreServices.discovery, }, async factory() { return async ({ discoveryApi }) => { diff --git a/plugins/catalog-react/.eslintrc.js b/plugins/catalog-react/.eslintrc.js index e2a53a6ad2..45bb6db521 100644 --- a/plugins/catalog-react/.eslintrc.js +++ b/plugins/catalog-react/.eslintrc.js @@ -1 +1,5 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'testing-library/prefer-screen-queries': 'error', + }, +}); diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 3019049086..5bc4167626 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,81 @@ # @backstage/plugin-catalog-react +## 1.2.2-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## 1.2.2-next.0 + +### Patch Changes + +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.2 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-permission-react@0.4.8-next.0 + +## 1.2.1 + +### Patch Changes + +- a889314692: Both `EntityProvider` and `AsyncEntityProvider` contexts now wrap all children with an `AnalyticsContext` containing the corresponding `entityRef`; this opens up the possibility for all events underneath these contexts to be associated with and aggregated by the corresponding entity. +- e47f466f80: Removed forced capitalization for Entity types in the catalog sidebar. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/version-bridge@1.0.2 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/types@1.0.1 + - @backstage/catalog-client@1.1.2 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-permission-react@0.4.7 + +## 1.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/version-bridge@1.0.1 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-permission-react@0.4.7-next.0 + ## 1.2.1-next.0 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index c2363fdc76..11b9555a12 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "1.2.1-next.0", + "version": "1.2.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -54,7 +54,7 @@ "qs": "^6.9.4", "react-use": "^17.2.4", "yaml": "^2.0.0", - "zen-observable": "^0.8.15" + "zen-observable": "^0.10.0" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx index 65ad3f227a..8a6b7206c6 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx @@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model'; import { ApiProvider } from '@backstage/core-app-api'; import { alertApiRef } from '@backstage/core-plugin-api'; import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; -import { fireEvent, waitFor } from '@testing-library/react'; +import { fireEvent, waitFor, screen } from '@testing-library/react'; import { capitalize } from 'lodash'; import { default as React } from 'react'; import { catalogApiRef } from '../../api'; @@ -75,7 +75,7 @@ describe('', () => { ); it('renders available entity kinds', async () => { - const rendered = await renderWithEffects( + await renderWithEffects( ', () => { , ); - expect(rendered.getByText('Kind')).toBeInTheDocument(); + expect(screen.getByText('Kind')).toBeInTheDocument(); - const input = rendered.getByTestId('select'); + const input = screen.getByTestId('select'); fireEvent.click(input); - await waitFor(() => rendered.getByText('Domain')); + await waitFor(() => screen.getByText('Domain')); entities.forEach(entity => { expect( - rendered.getByRole('option', { + screen.getByRole('option', { name: capitalize(entity.kind as string), }), ).toBeInTheDocument(); @@ -102,7 +102,7 @@ describe('', () => { it('sets the selected kind filter', async () => { const updateFilters = jest.fn(); - const rendered = await renderWithEffects( + await renderWithEffects( ', () => { , ); - const input = rendered.getByTestId('select'); + const input = screen.getByTestId('select'); fireEvent.click(input); - await waitFor(() => rendered.getByText('Domain')); - fireEvent.click(rendered.getByText('Domain')); + await waitFor(() => screen.getByText('Domain')); + fireEvent.click(screen.getByText('Domain')); expect(updateFilters).toHaveBeenLastCalledWith({ kind: new EntityKindFilter('domain'), diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx index 47e85292e2..f925625b23 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; import { EntityLifecycleFilter } from '../../filters'; @@ -56,36 +56,36 @@ const sampleEntities: Entity[] = [ describe('', () => { it('renders all lifecycles', () => { - const rendered = render( + render( , ); - expect(rendered.getByText('Lifecycle')).toBeInTheDocument(); + expect(screen.getByText('Lifecycle')).toBeInTheDocument(); - fireEvent.click(rendered.getByTestId('lifecycle-picker-expand')); + fireEvent.click(screen.getByTestId('lifecycle-picker-expand')); sampleEntities .map(e => e.spec?.lifecycle!) .forEach(lifecycle => { - expect(rendered.getByText(lifecycle as string)).toBeInTheDocument(); + expect(screen.getByText(lifecycle as string)).toBeInTheDocument(); }); }); it('renders unique lifecycles in alphabetical order', () => { - const rendered = render( + render( , ); - expect(rendered.getByText('Lifecycle')).toBeInTheDocument(); + expect(screen.getByText('Lifecycle')).toBeInTheDocument(); - fireEvent.click(rendered.getByTestId('lifecycle-picker-expand')); + fireEvent.click(screen.getByTestId('lifecycle-picker-expand')); - expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([ + expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([ 'experimental', 'production', ]); @@ -114,7 +114,7 @@ describe('', () => { it('adds lifecycles to filters', () => { const updateFilters = jest.fn(); - const rendered = render( + render( ', () => { lifecycles: undefined, }); - fireEvent.click(rendered.getByTestId('lifecycle-picker-expand')); - fireEvent.click(rendered.getByText('production')); + fireEvent.click(screen.getByTestId('lifecycle-picker-expand')); + fireEvent.click(screen.getByText('production')); expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: new EntityLifecycleFilter(['production']), }); @@ -138,7 +138,7 @@ describe('', () => { it('removes lifecycles from filters', () => { const updateFilters = jest.fn(); - const rendered = render( + render( ', () => { expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: new EntityLifecycleFilter(['production']), }); - fireEvent.click(rendered.getByTestId('lifecycle-picker-expand')); - expect(rendered.getByLabelText('production')).toBeChecked(); + fireEvent.click(screen.getByTestId('lifecycle-picker-expand')); + expect(screen.getByLabelText('production')).toBeChecked(); - fireEvent.click(rendered.getByLabelText('production')); + fireEvent.click(screen.getByLabelText('production')); expect(updateFilters).toHaveBeenLastCalledWith({ lifecycles: undefined, }); diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx index f648b51456..c816810b10 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx @@ -15,7 +15,7 @@ */ import { Entity, parseEntityRef } from '@backstage/catalog-model'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; import { EntityOwnerFilter } from '../../filters'; @@ -69,36 +69,36 @@ const sampleEntities: Entity[] = [ describe('', () => { it('renders all owners', () => { - const rendered = render( + render( , ); - expect(rendered.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner')).toBeInTheDocument(); - fireEvent.click(rendered.getByTestId('owner-picker-expand')); + fireEvent.click(screen.getByTestId('owner-picker-expand')); sampleEntities .flatMap(e => e.relations?.map(r => parseEntityRef(r.targetRef).name)) .forEach(owner => { - expect(rendered.getByText(owner as string)).toBeInTheDocument(); + expect(screen.getByText(owner as string)).toBeInTheDocument(); }); }); it('renders unique owners in alphabetical order', () => { - const rendered = render( + render( , ); - expect(rendered.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner')).toBeInTheDocument(); - fireEvent.click(rendered.getByTestId('owner-picker-expand')); + fireEvent.click(screen.getByTestId('owner-picker-expand')); - expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([ + expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([ 'another-owner', 'some-owner', 'some-owner-2', @@ -128,7 +128,7 @@ describe('', () => { it('adds owners to filters', () => { const updateFilters = jest.fn(); - const rendered = render( + render( ', () => { owners: undefined, }); - fireEvent.click(rendered.getByTestId('owner-picker-expand')); - fireEvent.click(rendered.getByText('some-owner')); + fireEvent.click(screen.getByTestId('owner-picker-expand')); + fireEvent.click(screen.getByText('some-owner')); expect(updateFilters).toHaveBeenLastCalledWith({ owners: new EntityOwnerFilter(['some-owner']), }); @@ -152,7 +152,7 @@ describe('', () => { it('removes owners from filters', () => { const updateFilters = jest.fn(); - const rendered = render( + render( ', () => { expect(updateFilters).toHaveBeenLastCalledWith({ owners: new EntityOwnerFilter(['some-owner']), }); - fireEvent.click(rendered.getByTestId('owner-picker-expand')); - expect(rendered.getByLabelText('some-owner')).toBeChecked(); + fireEvent.click(screen.getByTestId('owner-picker-expand')); + expect(screen.getByLabelText('some-owner')).toBeChecked(); - fireEvent.click(rendered.getByLabelText('some-owner')); + fireEvent.click(screen.getByLabelText('some-owner')); expect(updateFilters).toHaveBeenLastCalledWith({ owner: undefined, }); diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx index 8e90e04148..00d36e5e13 100644 --- a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { EntityErrorFilter, EntityOrphanFilter } from '../../filters'; import { MockEntityListContextProvider } from '../../testUtils/providers'; @@ -52,23 +52,23 @@ const sampleEntities: Entity[] = [ describe('', () => { it('renders all processing status options', () => { - const rendered = render( + render( , ); - expect(rendered.getByText('Processing Status')).toBeInTheDocument(); + expect(screen.getByText('Processing Status')).toBeInTheDocument(); - fireEvent.click(rendered.getByTestId('processing-status-picker-expand')); - expect(rendered.getByText('Is Orphan')).toBeInTheDocument(); - expect(rendered.getByText('Has Error')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('processing-status-picker-expand')); + expect(screen.getByText('Is Orphan')).toBeInTheDocument(); + expect(screen.getByText('Has Error')).toBeInTheDocument(); }); it('adds orphan to orphan filter', () => { const updateFilters = jest.fn(); - const rendered = render( + render( ', () => { , ); - fireEvent.click(rendered.getByTestId('processing-status-picker-expand')); - fireEvent.click(rendered.getByText('Is Orphan')); + fireEvent.click(screen.getByTestId('processing-status-picker-expand')); + fireEvent.click(screen.getByText('Is Orphan')); expect(updateFilters).toHaveBeenCalledWith({ orphan: new EntityOrphanFilter(true), }); @@ -89,7 +89,7 @@ describe('', () => { it('adds error to error filter', () => { const updateFilters = jest.fn(); - const rendered = render( + render( ', () => { , ); - fireEvent.click(rendered.getByTestId('processing-status-picker-expand')); - fireEvent.click(rendered.getByText('Has Error')); + fireEvent.click(screen.getByTestId('processing-status-picker-expand')); + fireEvent.click(screen.getByText('Has Error')); expect(updateFilters).toHaveBeenCalledWith({ error: new EntityErrorFilter(true), }); @@ -110,7 +110,7 @@ describe('', () => { it('remove orphan from orphan filter', () => { const updateFilters = jest.fn(); - const rendered = render( + render( ', () => { , ); - fireEvent.click(rendered.getByTestId('processing-status-picker-expand')); - fireEvent.click(rendered.getByText('Is Orphan')); + fireEvent.click(screen.getByTestId('processing-status-picker-expand')); + fireEvent.click(screen.getByText('Is Orphan')); expect(updateFilters).toHaveBeenCalledWith({ orphan: undefined, }); @@ -131,7 +131,7 @@ describe('', () => { it('remove error from error filter', () => { const updateFilters = jest.fn(); - const rendered = render( + render( ', () => { , ); - fireEvent.click(rendered.getByTestId('processing-status-picker-expand')); - fireEvent.click(rendered.getByText('Has Error')); + fireEvent.click(screen.getByTestId('processing-status-picker-expand')); + fireEvent.click(screen.getByText('Has Error')); expect(updateFilters).toHaveBeenCalledWith({ error: undefined, }); diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.test.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.test.tsx index eaaaab39bc..b0a6b46bf5 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.test.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.test.tsx @@ -15,6 +15,7 @@ */ import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { entityRouteRef } from '../../routes'; import { EntityRefLink } from './EntityRefLink'; @@ -33,16 +34,13 @@ describe('', () => { lifecycle: 'production', }, }; - const { getByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, }, - ); + }); - expect(getByText('component:software')).toHaveAttribute( + expect(screen.getByText('component:software')).toHaveAttribute( 'href', '/catalog/default/component/software', ); @@ -62,15 +60,12 @@ describe('', () => { lifecycle: 'production', }, }; - const { getByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, }, - ); - expect(getByText('component:test/software')).toHaveAttribute( + }); + expect(screen.getByText('component:test/software')).toHaveAttribute( 'href', '/catalog/test/component/software', ); @@ -90,7 +85,7 @@ describe('', () => { lifecycle: 'production', }, }; - const { getByText } = await renderInTestApp( + await renderInTestApp( , { mountedRoutes: { @@ -98,7 +93,7 @@ describe('', () => { }, }, ); - expect(getByText('test/software')).toHaveAttribute( + expect(screen.getByText('test/software')).toHaveAttribute( 'href', '/catalog/test/component/software', ); @@ -110,15 +105,12 @@ describe('', () => { namespace: 'default', name: 'software', }; - const { getByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, }, - ); - expect(getByText('component:software')).toHaveAttribute( + }); + expect(screen.getByText('component:software')).toHaveAttribute( 'href', '/catalog/default/component/software', ); @@ -130,15 +122,12 @@ describe('', () => { namespace: 'test', name: 'software', }; - const { getByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, }, - ); - expect(getByText('component:test/software')).toHaveAttribute( + }); + expect(screen.getByText('component:test/software')).toHaveAttribute( 'href', '/catalog/test/component/software', ); @@ -150,7 +139,7 @@ describe('', () => { namespace: 'test', name: 'software', }; - const { getByText } = await renderInTestApp( + await renderInTestApp( , { mountedRoutes: { @@ -158,7 +147,7 @@ describe('', () => { }, }, ); - expect(getByText('test/software')).toHaveAttribute( + expect(screen.getByText('test/software')).toHaveAttribute( 'href', '/catalog/test/component/software', ); @@ -170,7 +159,7 @@ describe('', () => { namespace: 'test', name: 'software', }; - const { getByText } = await renderInTestApp( + await renderInTestApp( Custom Children , @@ -180,7 +169,7 @@ describe('', () => { }, }, ); - expect(getByText('Custom Children')).toHaveAttribute( + expect(screen.getByText('Custom Children')).toHaveAttribute( 'href', '/catalog/test/component/software', ); diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.test.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.test.tsx index 147e139749..4e0d52ef4e 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.test.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.test.tsx @@ -15,6 +15,7 @@ */ import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { entityRouteRef } from '../../routes'; import { EntityRefLinks } from './EntityRefLinks'; @@ -28,15 +29,12 @@ describe('', () => { name: 'software', }, ]; - const { getByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, }, - ); - expect(getByText('component:software')).toHaveAttribute( + }); + expect(screen.getByText('component:software')).toHaveAttribute( 'href', '/catalog/default/component/software', ); @@ -55,20 +53,17 @@ describe('', () => { name: 'interface', }, ]; - const { getByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name/*': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name/*': entityRouteRef, }, - ); - expect(getByText(',')).toBeInTheDocument(); - expect(getByText('component:software')).toHaveAttribute( + }); + expect(screen.getByText(',')).toBeInTheDocument(); + expect(screen.getByText('component:software')).toHaveAttribute( 'href', '/catalog/default/component/software', ); - expect(getByText('api:interface')).toHaveAttribute( + expect(screen.getByText('api:interface')).toHaveAttribute( 'href', '/catalog/default/api/interface', ); diff --git a/plugins/catalog-react/src/components/EntityRefLink/FetchedEntityRefLinks.test.tsx b/plugins/catalog-react/src/components/EntityRefLink/FetchedEntityRefLinks.test.tsx index 36e2c0450d..9db6511662 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/FetchedEntityRefLinks.test.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/FetchedEntityRefLinks.test.tsx @@ -16,6 +16,7 @@ import { FetchedEntityRefLinks } from './FetchedEntityRefLinks'; import { entityRouteRef } from '../../routes'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import { Entity } from '@backstage/catalog-model'; import React from 'react'; import { JsonObject } from '@backstage/types'; @@ -60,7 +61,7 @@ describe('', () => { }), }; - const rendered = await renderInTestApp( + await renderInTestApp( , @@ -71,12 +72,12 @@ describe('', () => { }, ); - expect(rendered.getByText('SOFTWARE')).toHaveAttribute( + expect(screen.getByText('SOFTWARE')).toHaveAttribute( 'href', '/catalog/default/component/software', ); - expect(rendered.getByText('INTERFACE')).toHaveAttribute( + expect(screen.getByText('INTERFACE')).toHaveAttribute( 'href', '/catalog/default/api/interface', ); @@ -111,7 +112,7 @@ describe('', () => { const catalogApi: Partial = {}; - const rendered = await renderInTestApp( + await renderInTestApp( , @@ -122,12 +123,12 @@ describe('', () => { }, ); - expect(rendered.getByText('TOOL')).toHaveAttribute( + expect(screen.getByText('TOOL')).toHaveAttribute( 'href', '/catalog/default/component/tool', ); - expect(rendered.getByText('IMPLEMENTATION')).toHaveAttribute( + expect(screen.getByText('IMPLEMENTATION')).toHaveAttribute( 'href', '/catalog/default/api/implementation', ); @@ -189,7 +190,7 @@ describe('', () => { }), }; - const rendered = await renderInTestApp( + await renderInTestApp( , @@ -200,17 +201,17 @@ describe('', () => { }, ); - expect(rendered.getByText('TOOL')).toHaveAttribute( + expect(screen.getByText('TOOL')).toHaveAttribute( 'href', '/catalog/default/component/tool', ); - expect(rendered.getByText('IMPLEMENTATION')).toHaveAttribute( + expect(screen.getByText('IMPLEMENTATION')).toHaveAttribute( 'href', '/catalog/default/api/implementation', ); - expect(rendered.getByText('INTERFACE')).toHaveAttribute( + expect(screen.getByText('INTERFACE')).toHaveAttribute( 'href', '/catalog/default/component/interface', ); diff --git a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx index 0b9ea9cb98..94db377e26 100644 --- a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, render, waitFor, screen } from '@testing-library/react'; import { EntitySearchBar } from './EntitySearchBar'; import { DefaultEntityFilters } from '../../hooks/useEntityListProvider'; import { EntityTextFilter } from '../../filters'; @@ -29,13 +29,13 @@ describe('EntitySearchBar', () => { text: new EntityTextFilter('hello'), }; - const { getByDisplayValue } = render( + render( , ); - const searchInput = getByDisplayValue('hello'); + const searchInput = screen.getByDisplayValue('hello'); expect(searchInput).toBeInTheDocument(); fireEvent.change(searchInput, { target: { value: 'world' } }); diff --git a/plugins/catalog-react/src/components/EntityTable/EntityTable.test.tsx b/plugins/catalog-react/src/components/EntityTable/EntityTable.test.tsx index 99f998118f..d5f1dc20d3 100644 --- a/plugins/catalog-react/src/components/EntityTable/EntityTable.test.tsx +++ b/plugins/catalog-react/src/components/EntityTable/EntityTable.test.tsx @@ -16,13 +16,13 @@ import { Entity } from '@backstage/catalog-model'; import { renderInTestApp } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import React from 'react'; import { EntityTable } from './EntityTable'; describe('', () => { it('shows empty table', async () => { - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { />, ); - expect(getByText('Entities')).toBeInTheDocument(); - expect(getByText('EMPTY')).toBeInTheDocument(); + expect(screen.getByText('Entities')).toBeInTheDocument(); + expect(screen.getByText('EMPTY')).toBeInTheDocument(); }); it('shows entities', async () => { @@ -47,7 +47,7 @@ describe('', () => { }, ]; - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { ); await waitFor(() => { - expect(getByText('my-entity')).toBeInTheDocument(); + expect(screen.getByText('my-entity')).toBeInTheDocument(); }); }); }); diff --git a/plugins/catalog-react/src/components/EntityTable/presets.test.tsx b/plugins/catalog-react/src/components/EntityTable/presets.test.tsx index cca9c7d494..82075df5e9 100644 --- a/plugins/catalog-react/src/components/EntityTable/presets.test.tsx +++ b/plugins/catalog-react/src/components/EntityTable/presets.test.tsx @@ -21,7 +21,7 @@ import { SystemEntity, } from '@backstage/catalog-model'; import { renderInTestApp } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import React from 'react'; import { entityRouteRef } from '../../routes'; import { EntityTable } from './EntityTable'; @@ -54,7 +54,7 @@ describe('systemEntityColumns', () => { }, ]; - const { getByText } = await renderInTestApp( + await renderInTestApp( { ); await waitFor(() => { - expect(getByText('my-namespace/my-system')).toBeInTheDocument(); - expect(getByText('my-namespace/my-domain')).toBeInTheDocument(); - expect(getByText('test')).toBeInTheDocument(); - expect(getByText(/Some/)).toBeInTheDocument(); + expect(screen.getByText('my-namespace/my-system')).toBeInTheDocument(); + expect(screen.getByText('my-namespace/my-domain')).toBeInTheDocument(); + expect(screen.getByText('test')).toBeInTheDocument(); + expect(screen.getByText(/Some/)).toBeInTheDocument(); }); }); }); @@ -106,7 +106,7 @@ describe('componentEntityColumns', () => { }, ]; - const { getByText } = await renderInTestApp( + await renderInTestApp( { ); await waitFor(() => { - expect(getByText('my-namespace/my-component')).toBeInTheDocument(); - expect(getByText('my-namespace/my-system')).toBeInTheDocument(); - expect(getByText('test')).toBeInTheDocument(); - expect(getByText('production')).toBeInTheDocument(); - expect(getByText('service')).toBeInTheDocument(); - expect(getByText(/Some/)).toBeInTheDocument(); + expect(screen.getByText('my-namespace/my-component')).toBeInTheDocument(); + expect(screen.getByText('my-namespace/my-system')).toBeInTheDocument(); + expect(screen.getByText('test')).toBeInTheDocument(); + expect(screen.getByText('production')).toBeInTheDocument(); + expect(screen.getByText('service')).toBeInTheDocument(); + expect(screen.getByText(/Some/)).toBeInTheDocument(); }); }); }); diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index 78bdde1994..7da357d806 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, render, waitFor, screen } from '@testing-library/react'; import React from 'react'; import { MockEntityListContextProvider } from '../../testUtils/providers'; import { EntityTagFilter } from '../../filters'; @@ -35,34 +35,34 @@ describe('', () => { } as unknown as CatalogApi; it('renders all tags', async () => { - const rendered = render( + render( , ); - await waitFor(() => expect(rendered.getByText('Tags')).toBeInTheDocument()); + await waitFor(() => expect(screen.getByText('Tags')).toBeInTheDocument()); - fireEvent.click(rendered.getByTestId('tag-picker-expand')); + fireEvent.click(screen.getByTestId('tag-picker-expand')); tags.forEach(tag => { - expect(rendered.getByText(tag)).toBeInTheDocument(); + expect(screen.getByText(tag)).toBeInTheDocument(); }); }); it('renders unique tags in alphabetical order', async () => { - const rendered = render( + render( , ); - await waitFor(() => expect(rendered.getByText('Tags')).toBeInTheDocument()); + await waitFor(() => expect(screen.getByText('Tags')).toBeInTheDocument()); - fireEvent.click(rendered.getByTestId('tag-picker-expand')); + fireEvent.click(screen.getByTestId('tag-picker-expand')); - expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([ + expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([ 'tag1', 'tag2', 'tag3', @@ -71,18 +71,18 @@ describe('', () => { }); it('renders tags with counts', async () => { - const rendered = render( + render( , ); - await waitFor(() => expect(rendered.getByText('Tags')).toBeInTheDocument()); + await waitFor(() => expect(screen.getByText('Tags')).toBeInTheDocument()); - fireEvent.click(rendered.getByTestId('tag-picker-expand')); + fireEvent.click(screen.getByTestId('tag-picker-expand')); - expect(rendered.getAllByRole('option').map(o => o.textContent)).toEqual([ + expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([ 'tag1 (0)', 'tag2 (1)', 'tag3 (2)', @@ -115,7 +115,7 @@ describe('', () => { it('adds tags to filters', async () => { const updateFilters = jest.fn(); - const rendered = render( + render( ', () => { }), ); - fireEvent.click(rendered.getByTestId('tag-picker-expand')); - fireEvent.click(rendered.getByText('tag1')); + fireEvent.click(screen.getByTestId('tag-picker-expand')); + fireEvent.click(screen.getByText('tag1')); expect(updateFilters).toHaveBeenLastCalledWith({ tags: new EntityTagFilter(['tag1']), }); @@ -141,7 +141,7 @@ describe('', () => { it('removes tags from filters', async () => { const updateFilters = jest.fn(); - const rendered = render( + render( ', () => { tags: new EntityTagFilter(['tag1']), }), ); - fireEvent.click(rendered.getByTestId('tag-picker-expand')); - expect(rendered.getByLabelText('tag1')).toBeChecked(); + fireEvent.click(screen.getByTestId('tag-picker-expand')); + expect(screen.getByLabelText('tag1')).toBeChecked(); - fireEvent.click(rendered.getByLabelText('tag1')); + fireEvent.click(screen.getByLabelText('tag1')); expect(updateFilters).toHaveBeenLastCalledWith({ tags: undefined, }); diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index 525c757dea..d61a6c480f 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -15,13 +15,12 @@ */ import React from 'react'; -import { fireEvent, waitFor } from '@testing-library/react'; +import { fireEvent, waitFor, screen } from '@testing-library/react'; import { Entity } from '@backstage/catalog-model'; import { EntityTypePicker } from './EntityTypePicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; import { catalogApiRef } from '../../api'; import { EntityKindFilter, EntityTypeFilter } from '../../filters'; - import { alertApiRef } from '@backstage/core-plugin-api'; import { ApiProvider } from '@backstage/core-app-api'; import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; @@ -84,7 +83,7 @@ const apis = TestApiRegistry.from( describe('', () => { it('renders available entity types', async () => { - const rendered = await renderWithEffects( + await renderWithEffects( ', () => { , ); - expect(rendered.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); - const input = rendered.getByTestId('select'); + const input = screen.getByTestId('select'); fireEvent.click(input); - await waitFor(() => rendered.getByText('service')); + await waitFor(() => screen.getByText('service')); entities.forEach(entity => { - expect( - rendered.getByText(entity.spec!.type as string), - ).toBeInTheDocument(); + expect(screen.getByText(entity.spec!.type as string)).toBeInTheDocument(); }); }); it('sets the selected type filter', async () => { const updateFilters = jest.fn(); - const rendered = await renderWithEffects( + await renderWithEffects( ', () => { , ); - const input = rendered.getByTestId('select'); + const input = screen.getByTestId('select'); fireEvent.click(input); - await waitFor(() => rendered.getByText('service')); - fireEvent.click(rendered.getByText('service')); + await waitFor(() => screen.getByText('service')); + fireEvent.click(screen.getByText('service')); expect(updateFilters).toHaveBeenLastCalledWith({ type: new EntityTypeFilter(['service']), }); fireEvent.click(input); - fireEvent.click(rendered.getByText('all')); + fireEvent.click(screen.getByText('all')); expect(updateFilters).toHaveBeenLastCalledWith({ type: undefined }); }); diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 3db2fab0fb..c31381df20 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, render, waitFor, screen } from '@testing-library/react'; import { Entity, RELATION_OWNED_BY, @@ -27,7 +27,6 @@ import { EntityTagFilter, UserListFilter } from '../../filters'; import { CatalogApi } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; import { MockStorageApi, TestApiRegistry } from '@backstage/test-utils'; - import { ApiProvider } from '@backstage/core-app-api'; import { ConfigApi, @@ -144,7 +143,7 @@ const backendEntities: Entity[] = [ describe('', () => { it('renders filter groups', () => { - const { queryByText } = render( + render( @@ -152,12 +151,12 @@ describe('', () => { , ); - expect(queryByText('Personal')).toBeInTheDocument(); - expect(queryByText('Test Company')).toBeInTheDocument(); + expect(screen.getByText('Personal')).toBeInTheDocument(); + expect(screen.getByText('Test Company')).toBeInTheDocument(); }); it('renders filters', () => { - const { getAllByRole } = render( + render( @@ -166,12 +165,12 @@ describe('', () => { ); expect( - getAllByRole('menuitem').map(({ textContent }) => textContent), + screen.getAllByRole('menuitem').map(({ textContent }) => textContent), ).toEqual(['Owned 1', 'Starred 1', 'All 4']); }); it('includes counts alongside each filter', async () => { - const { getAllByRole } = render( + render( @@ -183,13 +182,13 @@ describe('', () => { // menuitem itself, so we pick off the next sibling. await waitFor(() => { expect( - getAllByRole('menuitem').map(({ textContent }) => textContent), + screen.getAllByRole('menuitem').map(({ textContent }) => textContent), ).toEqual(['Owned 1', 'Starred 1', 'All 4']); }); }); it('respects other frontend filters in counts', async () => { - const { getAllByRole } = render( + render( ', () => { await waitFor(() => { expect( - getAllByRole('menuitem').map(({ textContent }) => textContent), + screen.getAllByRole('menuitem').map(({ textContent }) => textContent), ).toEqual(['Owned 1', 'Starred 0', 'All 2']); }); }); @@ -229,7 +228,7 @@ describe('', () => { it('updates user filter when a menuitem is selected', () => { const updateFilters = jest.fn(); - const { getByText } = render( + render( ', () => { , ); - fireEvent.click(getByText('Starred')); + fireEvent.click(screen.getByText('Starred')); expect(updateFilters).toHaveBeenLastCalledWith({ user: new UserListFilter( diff --git a/plugins/catalog/.eslintrc.js b/plugins/catalog/.eslintrc.js index e2a53a6ad2..45bb6db521 100644 --- a/plugins/catalog/.eslintrc.js +++ b/plugins/catalog/.eslintrc.js @@ -1 +1,5 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'testing-library/prefer-screen-queries': 'error', + }, +}); diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index e95afd8617..787fe48d4b 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,84 @@ # @backstage/plugin-catalog +## 1.6.2-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- ca04d97b09: Handle refresh entity error in `AboutCard`. +- a19cffbeed: Update search links to only have header as linkable text +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.6.2-next.0 + +### Patch Changes + +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- 387d1d5218: Fixed Entity kind pluralisation in the `CatalogKindHeader` component. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## 1.6.1 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-search-react@1.2.1 + - @backstage/catalog-client@1.1.2 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-search-common@1.1.1 + +## 1.6.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + ## 1.6.1-next.0 ### Patch Changes diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 3f72df00e2..74f64882a8 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -73,7 +73,7 @@ export const CatalogEntityPage: () => JSX.Element; // @public (undocumented) export const CatalogIndexPage: (props: DefaultCatalogPageProps) => JSX.Element; -// @public (undocumented) +// @public @deprecated (undocumented) export function CatalogKindHeader(props: CatalogKindHeaderProps): JSX.Element; // @public diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index de2833c470..90324e8ac9 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "1.6.1-next.0", + "version": "1.6.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -49,9 +49,10 @@ "@material-ui/lab": "4.0.0-alpha.57", "history": "^5.0.0", "lodash": "^4.17.21", + "pluralize": "^8.0.0", "react-helmet": "6.1.0", "react-use": "^17.2.4", - "zen-observable": "^0.8.15" + "zen-observable": "^0.10.0" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index c77787f717..869d6f1757 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -28,6 +28,7 @@ import { } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; +import { screen } from '@testing-library/react'; import React from 'react'; import { viewTechDocRouteRef } from '../../routes'; import { AboutCard } from './AboutCard'; @@ -73,7 +74,7 @@ describe('', () => { ], }; - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { }, ); - expect(getByText('service')).toBeInTheDocument(); - expect(getByText('user:guest')).toBeInTheDocument(); - expect(getByText('production')).toBeInTheDocument(); - expect(getByText('This is the description')).toBeInTheDocument(); + expect(screen.getByText('service')).toBeInTheDocument(); + expect(screen.getByText('user:guest')).toBeInTheDocument(); + expect(screen.getByText('production')).toBeInTheDocument(); + expect(screen.getByText('This is the description')).toBeInTheDocument(); }); it('renders "view source" link', async () => { @@ -122,7 +123,7 @@ describe('', () => { }, }; - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { }, }, ); - expect(getByText('View Source').closest('a')).toHaveAttribute( + expect(screen.getByText('View Source').closest('a')).toHaveAttribute( 'href', 'https://github.com/backstage/backstage/blob/master/software.yaml', ); @@ -177,7 +178,7 @@ describe('', () => { }, }; - const { getByTitle } = await renderInTestApp( + await renderInTestApp( ', () => { }, ); - const editLink = getByTitle('Edit Metadata').closest('a'); + const editLink = screen.getByTitle('Edit Metadata').closest('a'); expect(editLink).toHaveAttribute( 'href', 'https://github.com/backstage/backstage/edit/master/software.yaml', @@ -230,7 +231,7 @@ describe('', () => { }, }; - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { }, }, ); - expect(getByText('View Source')).toBeVisible(); - expect(getByText('View Source').closest('a')).toBeNull(); + expect(screen.getByText('View Source')).toBeVisible(); + expect(screen.getByText('View Source').closest('a')).toBeNull(); }); it.each([ @@ -274,7 +275,7 @@ describe('', () => { }, }; - const { getByTitle } = await renderInTestApp( + await renderInTestApp( ', () => { 'component:default/software', ); - await userEvent.click(getByTitle('Schedule entity refresh')); + await userEvent.click(screen.getByTitle('Schedule entity refresh')); expect(catalogApi.refreshEntity).toHaveBeenCalledWith( 'component:default/software', @@ -320,7 +321,7 @@ describe('', () => { }, }; - const { queryByTitle } = await renderInTestApp( + await renderInTestApp( ', () => { }, ); - expect(queryByTitle('Schedule entity refresh')).not.toBeInTheDocument(); + expect( + screen.queryByTitle('Schedule entity refresh'), + ).not.toBeInTheDocument(); }); it('renders techdocs link', async () => { @@ -361,7 +364,7 @@ describe('', () => { }, }; - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { }, ); - expect(getByText('View TechDocs').closest('a')).toHaveAttribute( + expect(screen.getByText('View TechDocs').closest('a')).toHaveAttribute( 'href', '/docs/default/Component/software', ); @@ -414,7 +417,7 @@ describe('', () => { }, }; - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { }, ); - expect(getByText('View TechDocs')).toBeVisible(); - expect(getByText('View TechDocs').closest('a')).toBeNull(); + expect(screen.getByText('View TechDocs')).toBeVisible(); + expect(screen.getByText('View TechDocs').closest('a')).toBeNull(); }); it('renders disabled techdocs link when route is not bound', async () => { @@ -467,7 +470,7 @@ describe('', () => { }, }; - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { }, ); - expect(getByText('View TechDocs')).toBeVisible(); - expect(getByText('View TechDocs').closest('a')).toBeNull(); + expect(screen.getByText('View TechDocs')).toBeVisible(); + expect(screen.getByText('View TechDocs').closest('a')).toBeNull(); }); }); diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index b7ccd49b31..cb48be2b26 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -26,7 +26,12 @@ import { InfoCardVariants, Link, } from '@backstage/core-components'; -import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { + alertApiRef, + errorApiRef, + useApi, + useRouteRef, +} from '@backstage/core-plugin-api'; import { ScmIntegrationIcon, scmIntegrationsApiRef, @@ -90,6 +95,7 @@ export function AboutCard(props: AboutCardProps) { const scmIntegrationsApi = useApi(scmIntegrationsApiRef); const catalogApi = useApi(catalogApiRef); const alertApi = useApi(alertApiRef); + const errorApi = useApi(errorApiRef); const viewTechdocLink = useRouteRef(viewTechDocRouteRef); const entitySourceLocation = getEntitySourceLocation( @@ -139,9 +145,13 @@ export function AboutCard(props: AboutCardProps) { const allowRefresh = entityLocation?.startsWith('url:') || entityLocation?.startsWith('file:'); const refreshEntity = useCallback(async () => { - await catalogApi.refreshEntity(stringifyEntityRef(entity)); - alertApi.post({ message: 'Refresh scheduled', severity: 'info' }); - }, [catalogApi, alertApi, entity]); + try { + await catalogApi.refreshEntity(stringifyEntityRef(entity)); + alertApi.post({ message: 'Refresh scheduled', severity: 'info' }); + } catch (e) { + errorApi.post(e); + } + }, [catalogApi, alertApi, errorApi, entity]); return ( diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.test.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.test.tsx index f4dbb73b45..2a89f2fd92 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.test.tsx @@ -21,6 +21,7 @@ import { } from '@backstage/catalog-model'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { AboutContent } from './AboutContent'; @@ -62,32 +63,29 @@ describe('', () => { }); it('renders info', async () => { - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('user:o'); - expect(getByText('Domain')).toBeInTheDocument(); - expect(getByText('Domain').nextSibling).toHaveTextContent('d'); - expect(getByText('System')).toBeInTheDocument(); - expect(getByText('System').nextSibling).toHaveTextContent('s'); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(getByText('Type')).toBeInTheDocument(); - expect(getByText('Type').nextSibling).toHaveTextContent('t'); - expect(getByText('Lifecycle')).toBeInTheDocument(); - expect(getByText('Lifecycle').nextSibling).toHaveTextContent('l'); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1'); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent('user:o'); + expect(screen.getByText('Domain')).toBeInTheDocument(); + expect(screen.getByText('Domain').nextSibling).toHaveTextContent('d'); + expect(screen.getByText('System')).toBeInTheDocument(); + expect(screen.getByText('System').nextSibling).toHaveTextContent('s'); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Type').nextSibling).toHaveTextContent('t'); + expect(screen.getByText('Lifecycle')).toBeInTheDocument(); + expect(screen.getByText('Lifecycle').nextSibling).toHaveTextContent('l'); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1'); }); it('highlights missing required fields', async () => { @@ -95,26 +93,25 @@ describe('', () => { entity.spec = {}; entity.relations = []; - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(queryByText('System')).not.toBeInTheDocument(); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(queryByText('Type')).not.toBeInTheDocument(); - expect(queryByText('Lifecycle')).not.toBeInTheDocument(); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'No Owner', + ); + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.queryByText('System')).not.toBeInTheDocument(); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.queryByText('Type')).not.toBeInTheDocument(); + expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument(); }); }); @@ -151,33 +148,34 @@ describe('', () => { }); it('renders info', async () => { - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(getByText('System')).toBeInTheDocument(); - expect(getByText('System').nextSibling).toHaveTextContent('system'); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(getByText('Type')).toBeInTheDocument(); - expect(getByText('Type').nextSibling).toHaveTextContent('openapi'); - expect(getByText('Lifecycle')).toBeInTheDocument(); - expect(getByText('Lifecycle').nextSibling).toHaveTextContent( + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'user:guest', + ); + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.getByText('System')).toBeInTheDocument(); + expect(screen.getByText('System').nextSibling).toHaveTextContent( + 'system', + ); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Type').nextSibling).toHaveTextContent('openapi'); + expect(screen.getByText('Lifecycle')).toBeInTheDocument(); + expect(screen.getByText('Lifecycle').nextSibling).toHaveTextContent( 'production', ); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1'); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1'); }); it('highlights missing required fields', async () => { @@ -187,31 +185,34 @@ describe('', () => { delete entity.spec!.system; entity.relations = []; - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(getByText('System')).toBeInTheDocument(); - expect(getByText('System').nextSibling).toHaveTextContent('No System'); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(getByText('Type')).toBeInTheDocument(); - expect(getByText('Type').nextSibling).toHaveTextContent('unknown'); - expect(getByText('Lifecycle')).toBeInTheDocument(); - expect(getByText('Lifecycle').nextSibling).toHaveTextContent('unknown'); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags'); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'No Owner', + ); + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.getByText('System')).toBeInTheDocument(); + expect(screen.getByText('System').nextSibling).toHaveTextContent( + 'No System', + ); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Type').nextSibling).toHaveTextContent('unknown'); + expect(screen.getByText('Lifecycle')).toBeInTheDocument(); + expect(screen.getByText('Lifecycle').nextSibling).toHaveTextContent( + 'unknown', + ); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags'); }); }); @@ -252,36 +253,37 @@ describe('', () => { }); it('renders info', async () => { - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(getByText('System')).toBeInTheDocument(); - expect(getByText('System').nextSibling).toHaveTextContent('system'); - expect(getByText('Parent Component')).toBeInTheDocument(); - expect(getByText('Parent Component').nextSibling).toHaveTextContent( - 'parent-software', + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'user:guest', ); - expect(getByText('Type')).toBeInTheDocument(); - expect(getByText('Type').nextSibling).toHaveTextContent('service'); - expect(getByText('Lifecycle')).toBeInTheDocument(); - expect(getByText('Lifecycle').nextSibling).toHaveTextContent( + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.getByText('System')).toBeInTheDocument(); + expect(screen.getByText('System').nextSibling).toHaveTextContent( + 'system', + ); + expect(screen.getByText('Parent Component')).toBeInTheDocument(); + expect( + screen.getByText('Parent Component').nextSibling, + ).toHaveTextContent('parent-software'); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Type').nextSibling).toHaveTextContent('service'); + expect(screen.getByText('Lifecycle')).toBeInTheDocument(); + expect(screen.getByText('Lifecycle').nextSibling).toHaveTextContent( 'production', ); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1'); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1'); }); it('highlights missing required fields', async () => { @@ -291,31 +293,34 @@ describe('', () => { delete entity.spec!.system; entity.relations = []; - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(getByText('System')).toBeInTheDocument(); - expect(getByText('System').nextSibling).toHaveTextContent('No System'); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(getByText('Type')).toBeInTheDocument(); - expect(getByText('Type').nextSibling).toHaveTextContent('unknown'); - expect(getByText('Lifecycle')).toBeInTheDocument(); - expect(getByText('Lifecycle').nextSibling).toHaveTextContent('unknown'); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags'); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'No Owner', + ); + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.getByText('System')).toBeInTheDocument(); + expect(screen.getByText('System').nextSibling).toHaveTextContent( + 'No System', + ); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Type').nextSibling).toHaveTextContent('unknown'); + expect(screen.getByText('Lifecycle')).toBeInTheDocument(); + expect(screen.getByText('Lifecycle').nextSibling).toHaveTextContent( + 'unknown', + ); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags'); }); }); @@ -344,56 +349,54 @@ describe('', () => { }); it('renders info', async () => { - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(queryByText('System')).not.toBeInTheDocument(); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(queryByText('Type')).not.toBeInTheDocument(); - expect(queryByText('Lifecycle')).not.toBeInTheDocument(); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1'); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'user:guest', + ); + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.queryByText('System')).not.toBeInTheDocument(); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.queryByText('Type')).not.toBeInTheDocument(); + expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1'); }); it('highlights missing required fields', async () => { delete entity.metadata.tags; entity.relations = []; - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(queryByText('System')).not.toBeInTheDocument(); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(queryByText('Type')).not.toBeInTheDocument(); - expect(queryByText('Lifecycle')).not.toBeInTheDocument(); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags'); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'No Owner', + ); + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.queryByText('System')).not.toBeInTheDocument(); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.queryByText('Type')).not.toBeInTheDocument(); + expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags'); }); }); @@ -418,31 +421,30 @@ describe('', () => { }); it('renders info', async () => { - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(queryByText('System')).not.toBeInTheDocument(); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(getByText('Type')).toBeInTheDocument(); - expect(getByText('Type').nextSibling).toHaveTextContent('root'); - expect(queryByText('Lifecycle')).not.toBeInTheDocument(); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1'); - expect(getByText('Targets')).toBeInTheDocument(); - expect(getByText('Targets').nextSibling).toHaveTextContent( + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'No Owner', + ); + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.queryByText('System')).not.toBeInTheDocument(); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Type').nextSibling).toHaveTextContent('root'); + expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1'); + expect(screen.getByText('Targets')).toBeInTheDocument(); + expect(screen.getByText('Targets').nextSibling).toHaveTextContent( 'https://backstage.io', ); }); @@ -451,29 +453,28 @@ describe('', () => { delete entity.metadata.tags; delete entity.spec!.type; - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(queryByText('System')).not.toBeInTheDocument(); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(getByText('Type')).toBeInTheDocument(); - expect(getByText('Type').nextSibling).toHaveTextContent('unknown'); - expect(queryByText('Lifecycle')).not.toBeInTheDocument(); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags'); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'No Owner', + ); + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.queryByText('System')).not.toBeInTheDocument(); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Type').nextSibling).toHaveTextContent('unknown'); + expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags'); }); }); @@ -508,30 +509,31 @@ describe('', () => { }); it('renders info', async () => { - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(getByText('System')).toBeInTheDocument(); - expect(getByText('System').nextSibling).toHaveTextContent('system'); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(getByText('Type')).toBeInTheDocument(); - expect(getByText('Type').nextSibling).toHaveTextContent('s3'); - expect(queryByText('Lifecycle')).not.toBeInTheDocument(); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1'); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'user:guest', + ); + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.getByText('System')).toBeInTheDocument(); + expect(screen.getByText('System').nextSibling).toHaveTextContent( + 'system', + ); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Type').nextSibling).toHaveTextContent('s3'); + expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1'); }); it('highlights missing required fields', async () => { @@ -540,30 +542,31 @@ describe('', () => { delete entity.spec!.system; entity.relations = []; - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner'); - expect(queryByText('Domain')).not.toBeInTheDocument(); - expect(getByText('System')).toBeInTheDocument(); - expect(getByText('System').nextSibling).toHaveTextContent('No System'); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(getByText('Type')).toBeInTheDocument(); - expect(getByText('Type').nextSibling).toHaveTextContent('unknown'); - expect(queryByText('Lifecycle')).not.toBeInTheDocument(); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags'); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'No Owner', + ); + expect(screen.queryByText('Domain')).not.toBeInTheDocument(); + expect(screen.getByText('System')).toBeInTheDocument(); + expect(screen.getByText('System').nextSibling).toHaveTextContent( + 'No System', + ); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Type').nextSibling).toHaveTextContent('unknown'); + expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags'); }); }); @@ -597,29 +600,30 @@ describe('', () => { }); it('renders info', async () => { - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest'); - expect(getByText('Domain')).toBeInTheDocument(); - expect(getByText('Domain').nextSibling).toHaveTextContent('domain'); - expect(queryByText('System')).not.toBeInTheDocument(); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(queryByText('Type')).not.toBeInTheDocument(); - expect(queryByText('Lifecycle')).not.toBeInTheDocument(); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1'); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'user:guest', + ); + expect(screen.getByText('Domain')).toBeInTheDocument(); + expect(screen.getByText('Domain').nextSibling).toHaveTextContent( + 'domain', + ); + expect(screen.queryByText('System')).not.toBeInTheDocument(); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.queryByText('Type')).not.toBeInTheDocument(); + expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('tag-1'); }); it('highlights missing required fields', async () => { @@ -627,29 +631,30 @@ describe('', () => { delete entity.spec!.domain; entity.relations = []; - const { getByText, queryByText } = await renderInTestApp( - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, + await renderInTestApp(, { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, }, - ); + }); - expect(getByText('Description')).toBeInTheDocument(); - expect(getByText('Description').nextSibling).toHaveTextContent( + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Description').nextSibling).toHaveTextContent( 'This is the description', ); - expect(getByText('Owner')).toBeInTheDocument(); - expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner'); - expect(getByText('Domain')).toBeInTheDocument(); - expect(getByText('Domain').nextSibling).toHaveTextContent('No Domain'); - expect(queryByText('System')).not.toBeInTheDocument(); - expect(queryByText('Parent Component')).not.toBeInTheDocument(); - expect(queryByText('Type')).not.toBeInTheDocument(); - expect(queryByText('Lifecycle')).not.toBeInTheDocument(); - expect(getByText('Tags')).toBeInTheDocument(); - expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags'); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Owner').nextSibling).toHaveTextContent( + 'No Owner', + ); + expect(screen.getByText('Domain')).toBeInTheDocument(); + expect(screen.getByText('Domain').nextSibling).toHaveTextContent( + 'No Domain', + ); + expect(screen.queryByText('System')).not.toBeInTheDocument(); + expect(screen.queryByText('Parent Component')).not.toBeInTheDocument(); + expect(screen.queryByText('Type')).not.toBeInTheDocument(); + expect(screen.queryByText('Lifecycle')).not.toBeInTheDocument(); + expect(screen.getByText('Tags')).toBeInTheDocument(); + expect(screen.getByText('Tags').nextSibling).toHaveTextContent('No Tags'); }); }); }); diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index 15d41f3ab3..f511f58c58 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { fireEvent } from '@testing-library/react'; +import { fireEvent, screen } from '@testing-library/react'; import { GetEntityFacetsResponse } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { @@ -31,6 +31,7 @@ import { } from '@backstage/test-utils'; import { CatalogKindHeader } from './CatalogKindHeader'; import { errorApiRef } from '@backstage/core-plugin-api'; +import pluralize from 'pluralize'; const entities: Entity[] = [ { @@ -83,7 +84,7 @@ const apis = TestApiRegistry.from( describe('', () => { it('renders available kinds', async () => { - const rendered = await renderWithEffects( + await renderWithEffects( @@ -91,18 +92,18 @@ describe('', () => { , ); - const input = rendered.getByText('Components'); + const input = screen.getByText('Components'); fireEvent.mouseDown(input); entities.map(entity => { expect( - rendered.getByRole('option', { name: `${entity.kind}s` }), + screen.getByRole('option', { name: `${pluralize(entity.kind)}` }), ).toBeInTheDocument(); }); }); it('renders unknown kinds provided in query parameters', async () => { - const rendered = await renderWithEffects( + await renderWithEffects( ', () => { , ); - expect(rendered.getByText('Frobs')).toBeInTheDocument(); + expect(screen.getByText('Frobs')).toBeInTheDocument(); }); it('updates the kind filter', async () => { const updateFilters = jest.fn(); - const rendered = await renderWithEffects( + await renderWithEffects( @@ -125,10 +126,10 @@ describe('', () => { , ); - const input = rendered.getByText('Components'); + const input = screen.getByText('Components'); fireEvent.mouseDown(input); - const option = rendered.getByRole('option', { name: 'Templates' }); + const option = screen.getByRole('option', { name: 'Templates' }); fireEvent.click(option); expect(updateFilters).toHaveBeenCalledWith({ @@ -171,7 +172,7 @@ describe('', () => { }); it('limits kinds when allowedKinds is set', async () => { - const rendered = await renderWithEffects( + await renderWithEffects( @@ -179,22 +180,20 @@ describe('', () => { , ); - const input = rendered.getByText('Components'); + const input = screen.getByText('Components'); fireEvent.mouseDown(input); expect( - rendered.getByRole('option', { name: 'Components' }), + screen.getByRole('option', { name: 'Components' }), ).toBeInTheDocument(); + expect(screen.getByRole('option', { name: 'Systems' })).toBeInTheDocument(); expect( - rendered.getByRole('option', { name: 'Systems' }), - ).toBeInTheDocument(); - expect( - rendered.queryByRole('option', { name: 'Templates' }), + screen.queryByRole('option', { name: 'Templates' }), ).not.toBeInTheDocument(); }); it('renders kind from the query parameter even when not in allowedKinds', async () => { - const rendered = await renderWithEffects( + await renderWithEffects( ', () => { , ); - expect(rendered.getByText('Frobs')).toBeInTheDocument(); - const input = rendered.getByText('Frobs'); + expect(screen.getByText('Frobs')).toBeInTheDocument(); + const input = screen.getByText('Frobs'); fireEvent.mouseDown(input); - expect( - rendered.getByRole('option', { name: 'Systems' }), - ).toBeInTheDocument(); + expect(screen.getByRole('option', { name: 'Systems' })).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx index b0aaeae658..67631a3b98 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.tsx @@ -31,6 +31,7 @@ import { } from '@backstage/plugin-catalog-react'; import useAsync from 'react-use/lib/useAsync'; import { useApi } from '@backstage/core-plugin-api'; +import pluralize from 'pluralize'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -58,7 +59,10 @@ export interface CatalogKindHeaderProps { initialFilter?: string; } -/** @public */ +/** + * @public + * @deprecated This component has been deprecated in favour of the EntityKindPicker in the list of filters. If you wish to keep this component long term make sure to raise an issue at github.com/backstage/backstage + */ export function CatalogKindHeader(props: CatalogKindHeaderProps) { const { initialFilter = 'component', allowedKinds } = props; const classes = useStyles(); @@ -132,7 +136,7 @@ export function CatalogKindHeader(props: CatalogKindHeaderProps) { > {Object.keys(options).map(kind => ( - {`${options[kind]}s`} + {`${pluralize(options[kind])}`} ))} diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 6fbdcf40a2..5b80f5976c 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import { useOutlet } from 'react-router'; import { CatalogPage } from './CatalogPage'; @@ -30,15 +31,15 @@ jest.mock('./DefaultCatalogPage', () => ({ describe('CatalogPage', () => { it('renders provided router element', async () => { - const { getByText } = await renderInTestApp(); + await renderInTestApp(); - expect(getByText('Route Children')).toBeInTheDocument(); + expect(screen.getByText('Route Children')).toBeInTheDocument(); }); it('renders DefaultCatalogPage home when no router children are provided', async () => { (useOutlet as jest.Mock).mockReturnValueOnce(null); - const { getByText } = await renderInTestApp(); + await renderInTestApp(); - expect(getByText('DefaultCatalogPage')).toBeInTheDocument(); + expect(screen.getByText('DefaultCatalogPage')).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index cc00a57314..a2b26b87e1 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -23,21 +23,20 @@ import { } from '@backstage/catalog-model'; import { TableColumn, TableProps } from '@backstage/core-components'; import { - createPlugin, IdentityApi, identityApiRef, - PluginProvider, ProfileInfo, storageApiRef, } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef, - starredEntitiesApiRef, MockStarredEntitiesApi, + starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; import { mockBreakpoint, + MockPluginProvider, MockStorageApi, renderWithEffects, TestApiProvider, @@ -135,22 +134,6 @@ describe('DefaultCatalogPage', () => { }; const storageApi = MockStorageApi.create(); - type TestInputPluginOptions = { - 'key-1': string; - }; - - type TestPluginOptions = { - 'key-1': string; - 'key-2': string; - }; - - const plugin = createPlugin({ - id: 'my-plugin', - __experimentalConfigure(_: TestInputPluginOptions): TestPluginOptions { - return { 'key-1': 'value-1', 'key-2': 'value-2' }; - }, - }); - const renderWrapped = (children: React.ReactNode) => renderWithEffects( wrapInTestApp( @@ -162,7 +145,7 @@ describe('DefaultCatalogPage', () => { [starredEntitiesApiRef, new MockStarredEntitiesApi()], ]} > - {children} + {children} , { mountedRoutes: { @@ -178,11 +161,11 @@ describe('DefaultCatalogPage', () => { // limit. We should investigate why these timeouts happen. it('should render the default column of the grid', async () => { - const { getAllByRole } = await renderWrapped(); + await renderWrapped(); - const columnHeader = getAllByRole('button').filter( - c => c.tagName === 'SPAN', - ); + const columnHeader = screen + .getAllByRole('button') + .filter(c => c.tagName === 'SPAN'); const columnHeaderLabels = columnHeader.map(c => c.textContent); expect(columnHeaderLabels).toEqual([ @@ -203,26 +186,22 @@ describe('DefaultCatalogPage', () => { { title: 'Bar', field: 'entity.bar' }, { title: 'Baz', field: 'entity.spec.lifecycle' }, ]; - const { getAllByRole } = await renderWrapped( - , - ); + await renderWrapped(); - const columnHeader = getAllByRole('button').filter( - c => c.tagName === 'SPAN', - ); + const columnHeader = screen + .getAllByRole('button') + .filter(c => c.tagName === 'SPAN'); const columnHeaderLabels = columnHeader.map(c => c.textContent); expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']); }, 20_000); it('should render the default actions of an item in the grid', async () => { - const { getByTestId, findByTitle, findByText } = await renderWrapped( - , - ); - fireEvent.click(getByTestId('user-picker-owned')); - expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); - expect(await findByTitle(/View/)).toBeInTheDocument(); - expect(await findByTitle(/Edit/)).toBeInTheDocument(); - expect(await findByTitle(/Add to favorites/)).toBeInTheDocument(); + await renderWrapped(); + fireEvent.click(screen.getByTestId('user-picker-owned')); + expect(await screen.findByText(/Owned \(1\)/)).toBeInTheDocument(); + expect(await screen.findByTitle(/View/)).toBeInTheDocument(); + expect(await screen.findByTitle(/Edit/)).toBeInTheDocument(); + expect(await screen.findByTitle(/Add to favorites/)).toBeInTheDocument(); }, 20_000); it('should render the custom actions of an item passed as prop', async () => { @@ -245,41 +224,35 @@ describe('DefaultCatalogPage', () => { }, ]; - const { getByTestId, findByTitle, findByText } = await renderWrapped( - , - ); - fireEvent.click(getByTestId('user-picker-owned')); - expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); - expect(await findByTitle(/Foo Action/)).toBeInTheDocument(); - expect(await findByTitle(/Bar Action/)).toBeInTheDocument(); - expect((await findByTitle(/Bar Action/)).firstChild).toBeDisabled(); + await renderWrapped(); + fireEvent.click(screen.getByTestId('user-picker-owned')); + expect(await screen.findByText(/Owned \(1\)/)).toBeInTheDocument(); + expect(await screen.findByTitle(/Foo Action/)).toBeInTheDocument(); + expect(await screen.findByTitle(/Bar Action/)).toBeInTheDocument(); + expect((await screen.findByTitle(/Bar Action/)).firstChild).toBeDisabled(); }, 20_000); // this test right now causes some red lines in the log output when running tests // related to some theme issues in mui-table // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { - const { findByText, getByTestId } = await renderWrapped( - , - ); - fireEvent.click(getByTestId('user-picker-owned')); - await expect(findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); - fireEvent.click(getByTestId('user-picker-all')); - await expect(findByText(/All \(2\)/)).resolves.toBeInTheDocument(); + await renderWrapped(); + fireEvent.click(screen.getByTestId('user-picker-owned')); + await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); + fireEvent.click(screen.getByTestId('user-picker-all')); + await expect(screen.findByText(/All \(2\)/)).resolves.toBeInTheDocument(); }, 20_000); it('should set initial filter correctly', async () => { - const { findByText } = await renderWrapped( - , - ); - await expect(findByText(/All \(2\)/)).resolves.toBeInTheDocument(); + await renderWrapped(); + await expect(screen.findByText(/All \(2\)/)).resolves.toBeInTheDocument(); }, 20_000); // this test is for fixing the bug after favoriting an entity, the matching // entities defaulting to "owned" filter and not based on the selected filter it('should render the correct entities filtered on the selected filter', async () => { - const { getByTestId } = await renderWrapped(); - fireEvent.click(getByTestId('user-picker-owned')); + await renderWrapped(); + fireEvent.click(screen.getByTestId('user-picker-owned')); await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); // The "Starred" menu option should initially be disabled, since there // aren't any starred entities. @@ -308,10 +281,12 @@ describe('DefaultCatalogPage', () => { it('should wrap filter in drawer on smaller screens', async () => { mockBreakpoint({ matches: true }); - const { getByRole } = await renderWrapped(); - const button = getByRole('button', { name: 'Filters' }); - expect(getByRole('presentation', { hidden: true })).toBeInTheDocument(); + await renderWrapped(); + const button = screen.getByRole('button', { name: 'Filters' }); + expect( + screen.getByRole('presentation', { hidden: true }), + ).toBeInTheDocument(); fireEvent.click(button); - expect(getByRole('presentation')).toBeVisible(); + expect(screen.getByRole('presentation')).toBeVisible(); }, 20_000); }); diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx index c5208c94f5..644453f5fb 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -39,7 +39,6 @@ import { import React, { ReactNode } from 'react'; import { createComponentRouteRef } from '../../routes'; import { CatalogTable, CatalogTableRow } from '../CatalogTable'; -import { CatalogKindHeader } from '../CatalogKindHeader'; import { useCatalogPluginOptions } from '../../options'; /** @@ -73,17 +72,15 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) { return ( - - - } - > - - All your software catalog entities - + + + + All your software catalog entities + + @@ -103,8 +100,8 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) { /> - - + + ); } diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index a7307055c2..2e5cbe2899 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -60,6 +60,7 @@ export function CatalogSearchResultListItem( props: CatalogSearchResultListItemProps, ) { const result = props.result as any; + const highlight = props.highlight as ResultHighlight; const classes = useStyles(); const analytics = useAnalytics(); @@ -71,7 +72,7 @@ export function CatalogSearchResultListItem( }; return ( - + <> {props.icon && {props.icon}}
@@ -79,22 +80,24 @@ export function CatalogSearchResultListItem( className={classes.itemText} primaryTypographyProps={{ variant: 'h6' }} primary={ - props.highlight?.fields.title ? ( - - ) : ( - result.title - ) + + {highlight?.fields.title ? ( + + ) : ( + result.title + )} + } secondary={ - props.highlight?.fields.text ? ( + highlight?.fields.text ? ( ) : ( result.text @@ -112,6 +115,6 @@ export function CatalogSearchResultListItem(
- + ); } diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index bb2f302a37..652c9f3873 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -29,7 +29,7 @@ import { UserListFilter, } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; -import { act, fireEvent } from '@testing-library/react'; +import { act, fireEvent, screen } from '@testing-library/react'; import * as React from 'react'; import { CatalogTable } from './CatalogTable'; @@ -66,7 +66,7 @@ describe('CatalogTable component', () => { }); it('should render error message', async () => { - const rendered = await renderInTestApp( + await renderInTestApp( @@ -78,14 +78,14 @@ describe('CatalogTable component', () => { }, }, ); - const errorMessage = await rendered.findByText( + const errorMessage = await screen.findByText( /Could not fetch catalog entities./, ); expect(errorMessage).toBeInTheDocument(); }); it('should display entity names when loading has finished and no error occurred', async () => { - const rendered = await renderInTestApp( + await renderInTestApp( { }, }, ); - expect(rendered.getByText(/Owned \(3\)/)).toBeInTheDocument(); - expect(rendered.getByText(/component1/)).toBeInTheDocument(); - expect(rendered.getByText(/component2/)).toBeInTheDocument(); - expect(rendered.getByText(/component3/)).toBeInTheDocument(); + expect(screen.getByText(/Owned \(3\)/)).toBeInTheDocument(); + expect(screen.getByText(/component1/)).toBeInTheDocument(); + expect(screen.getByText(/component2/)).toBeInTheDocument(); + expect(screen.getByText(/component3/)).toBeInTheDocument(); }); it('should use specified edit URL if in annotation', async () => { @@ -124,7 +124,7 @@ describe('CatalogTable component', () => { }, }; - const { getByTitle } = await renderInTestApp( + await renderInTestApp( @@ -137,7 +137,7 @@ describe('CatalogTable component', () => { }, ); - const editButton = getByTitle('Edit'); + const editButton = screen.getByTitle('Edit'); await act(async () => { fireEvent.click(editButton); @@ -156,7 +156,7 @@ describe('CatalogTable component', () => { }, }; - const { getByTitle } = await renderInTestApp( + await renderInTestApp( @@ -169,7 +169,7 @@ describe('CatalogTable component', () => { }, ); - const viewButton = getByTitle('View'); + const viewButton = screen.getByTitle('View'); await act(async () => { fireEvent.click(viewButton); @@ -278,7 +278,7 @@ describe('CatalogTable component', () => { ])( 'should render correct columns with kind filter $kind', async ({ kind, expectedColumns }) => { - const { getAllByRole } = await renderInTestApp( + await renderInTestApp( { }, ); - const columnHeader = getAllByRole('button').filter( - c => c.tagName === 'SPAN', - ); + const columnHeader = screen + .getAllByRole('button') + .filter(c => c.tagName === 'SPAN'); const columnHeaderLabels = columnHeader.map(c => c.textContent); expect(columnHeaderLabels).toEqual(expectedColumns); }, @@ -316,7 +316,7 @@ describe('CatalogTable component', () => { }, }; - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -329,7 +329,7 @@ describe('CatalogTable component', () => { }, ); - expect(getByText('Should be rendered')).toBeInTheDocument(); + expect(screen.getByText('Should be rendered')).toBeInTheDocument(); }); it('should render the label column with customised title and value as specified', async () => { @@ -347,7 +347,7 @@ describe('CatalogTable component', () => { }; const expectedColumns = ['Name', 'Category', 'Actions']; - const { getAllByRole, getByText } = await renderInTestApp( + await renderInTestApp( @@ -360,13 +360,13 @@ describe('CatalogTable component', () => { }, ); - const columnHeader = getAllByRole('button').filter( - c => c.tagName === 'SPAN', - ); + const columnHeader = screen + .getAllByRole('button') + .filter(c => c.tagName === 'SPAN'); const columnHeaderLabels = columnHeader.map(c => c.textContent); expect(columnHeaderLabels).toEqual(expectedColumns); - const labelCellValue = getByText('generic'); + const labelCellValue = screen.getByText('generic'); expect(labelCellValue).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx index b3f8262076..94a9a86b70 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx @@ -22,7 +22,7 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import React from 'react'; import { DependencyOfComponentsCard } from './DependencyOfComponentsCard'; @@ -51,7 +51,7 @@ describe('', () => { relations: [], }; - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -64,9 +64,9 @@ describe('', () => { }, ); - expect(getByText('Dependency of components')).toBeInTheDocument(); + expect(screen.getByText('Dependency of components')).toBeInTheDocument(); expect( - getByText(/No component depends on this component/i), + screen.getByText(/No component depends on this component/i), ).toBeInTheDocument(); }); @@ -99,7 +99,7 @@ describe('', () => { ], }); - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -113,8 +113,8 @@ describe('', () => { ); await waitFor(() => { - expect(getByText('Dependency of components')).toBeInTheDocument(); - expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(screen.getByText('Dependency of components')).toBeInTheDocument(); + expect(screen.getByText(/target-name/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx index 57106015b2..657f95c60f 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx @@ -22,7 +22,7 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import React from 'react'; import { DependsOnComponentsCard } from './DependsOnComponentsCard'; @@ -51,7 +51,7 @@ describe('', () => { relations: [], }; - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -64,9 +64,9 @@ describe('', () => { }, ); - expect(getByText('Depends on components')).toBeInTheDocument(); + expect(screen.getByText('Depends on components')).toBeInTheDocument(); expect( - getByText(/No component is a dependency of this component/i), + screen.getByText(/No component is a dependency of this component/i), ).toBeInTheDocument(); }); @@ -99,7 +99,7 @@ describe('', () => { ], }); - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -113,8 +113,8 @@ describe('', () => { ); await waitFor(() => { - expect(getByText('Depends on components')).toBeInTheDocument(); - expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(screen.getByText('Depends on components')).toBeInTheDocument(); + expect(screen.getByText(/target-name/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx index bd52bfeeb8..5a77cd6ab4 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx @@ -22,7 +22,7 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import React from 'react'; import { DependsOnResourcesCard } from './DependsOnResourcesCard'; @@ -51,7 +51,7 @@ describe('', () => { relations: [], }; - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -64,9 +64,9 @@ describe('', () => { }, ); - expect(getByText('Depends on resources')).toBeInTheDocument(); + expect(screen.getByText('Depends on resources')).toBeInTheDocument(); expect( - getByText(/No resource is a dependency of this component/i), + screen.getByText(/No resource is a dependency of this component/i), ).toBeInTheDocument(); }); @@ -99,7 +99,7 @@ describe('', () => { ], }); - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -113,8 +113,8 @@ describe('', () => { ); await waitFor(() => { - expect(getByText('Depends on resources')).toBeInTheDocument(); - expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(screen.getByText('Depends on resources')).toBeInTheDocument(); + expect(screen.getByText(/target-name/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx index 04cfa1b947..f7220cb1ea 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx @@ -63,7 +63,7 @@ describe('ComponentContextMenu', () => { it('check Unregister entity button is disabled', async () => { const mockCallback = jest.fn(); - const { getByText } = await render( + await render( { expect(button).toBeInTheDocument(); fireEvent.click(button); - const unregister = await screen.getByText('Unregister entity'); + const unregister = screen.getByText('Unregister entity'); expect(unregister).toBeInTheDocument(); - const unregisterSpanItem = getByText(/Unregister entity/); + const unregisterSpanItem = screen.getByText(/Unregister entity/); const unregisterMenuListItem = unregisterSpanItem?.parentElement?.parentElement; expect(unregisterMenuListItem).toHaveAttribute('aria-disabled'); diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index 49952bdffe..f7a329c7be 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -89,7 +89,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) { const alertApi = useApi(alertApiRef); const copyToClipboard = useCallback(() => { - navigator.clipboard + window.navigator.clipboard .writeText(window.location.toString()) .then(() => alertApi.post({ message: 'Copied!', severity: 'info' })); }, [alertApi]); diff --git a/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.test.tsx b/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.test.tsx index 95cb601ea0..fd57b79431 100644 --- a/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.test.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.test.tsx @@ -60,7 +60,7 @@ describe('ComponentContextMenu', () => { it('check Unregister entity button is disabled', async () => { const mockCallback = jest.fn(); - const { getByText } = await render( + await render( { />, ); - const unregister = await screen.getByText('Unregister entity'); + const unregister = screen.getByText('Unregister entity'); expect(unregister).toBeInTheDocument(); - const unregisterSpanItem = getByText(/Unregister entity/); + const unregisterSpanItem = screen.getByText(/Unregister entity/); const unregisterMenuListItem = unregisterSpanItem?.parentElement?.parentElement; expect(unregisterMenuListItem).toHaveAttribute('aria-disabled'); diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 548fff3ff1..08c751c6c6 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -32,7 +32,7 @@ import { renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; -import { act, fireEvent } from '@testing-library/react'; +import { act, fireEvent, screen } from '@testing-library/react'; import React from 'react'; import { EntityLayout } from './EntityLayout'; @@ -52,7 +52,7 @@ const mockApis = TestApiRegistry.from( describe('EntityLayout', () => { it('renders simplest case', async () => { - const rendered = await renderInTestApp( + await renderInTestApp( @@ -69,9 +69,9 @@ describe('EntityLayout', () => { }, ); - expect(rendered.getByText('my-entity')).toBeInTheDocument(); - expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); - expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); + expect(screen.getByText('my-entity')).toBeInTheDocument(); + expect(screen.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(screen.getByText('tabbed-test-content')).toBeInTheDocument(); }); it('renders the entity title if defined', async () => { @@ -83,7 +83,7 @@ describe('EntityLayout', () => { }, } as Entity; - const rendered = await renderInTestApp( + await renderInTestApp( @@ -100,13 +100,13 @@ describe('EntityLayout', () => { }, ); - expect(rendered.getByText('My Entity')).toBeInTheDocument(); - expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); - expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); + expect(screen.getByText('My Entity')).toBeInTheDocument(); + expect(screen.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(screen.getByText('tabbed-test-content')).toBeInTheDocument(); }); it('renders default error message when entity is not found', async () => { - const rendered = await renderInTestApp( + await renderInTestApp( @@ -123,14 +123,14 @@ describe('EntityLayout', () => { }, ); - expect(rendered.getByText('Warning: Entity not found')).toBeInTheDocument(); - expect(rendered.queryByText('my-entity')).not.toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-title')).not.toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); + expect(screen.getByText('Warning: Entity not found')).toBeInTheDocument(); + expect(screen.queryByText('my-entity')).not.toBeInTheDocument(); + expect(screen.queryByText('tabbed-test-title')).not.toBeInTheDocument(); + expect(screen.queryByText('tabbed-test-content')).not.toBeInTheDocument(); }); it('renders custom message when entity is not found', async () => { - const rendered = await renderInTestApp( + await renderInTestApp( { ); expect( - rendered.getByText('Oppps.. Your entity was not found'), + screen.getByText('Oppps.. Your entity was not found'), ).toBeInTheDocument(); - expect(rendered.queryByText('my-entity')).not.toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-title')).not.toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); + expect(screen.queryByText('my-entity')).not.toBeInTheDocument(); + expect(screen.queryByText('tabbed-test-title')).not.toBeInTheDocument(); + expect(screen.queryByText('tabbed-test-content')).not.toBeInTheDocument(); }); it('navigates when user clicks different tab', async () => { - const rendered = await renderInTestApp( + await renderInTestApp( @@ -181,23 +181,23 @@ describe('EntityLayout', () => { }, ); - const secondTab = rendered.queryAllByRole('tab')[1]; + const secondTab = screen.queryAllByRole('tab')[1]; act(() => { fireEvent.click(secondTab); }); - expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); + expect(screen.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(screen.queryByText('tabbed-test-content')).not.toBeInTheDocument(); - expect(rendered.getByText('tabbed-test-title-2')).toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-content-2')).toBeInTheDocument(); + expect(screen.getByText('tabbed-test-title-2')).toBeInTheDocument(); + expect(screen.getByText('tabbed-test-content-2')).toBeInTheDocument(); }); it('should conditionally render tabs', async () => { const shouldRenderTab = (e: Entity) => e.metadata.name === 'my-entity'; const shouldNotRenderTab = (e: Entity) => e.metadata.name === 'some-entity'; - const rendered = await renderInTestApp( + await renderInTestApp( @@ -228,8 +228,8 @@ describe('EntityLayout', () => { }, ); - expect(rendered.queryByText('tabbed-test-title')).toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-title-2')).not.toBeInTheDocument(); - expect(rendered.queryByText('tabbed-test-title-3')).toBeInTheDocument(); + expect(screen.getByText('tabbed-test-title')).toBeInTheDocument(); + expect(screen.queryByText('tabbed-test-title-2')).not.toBeInTheDocument(); + expect(screen.getByText('tabbed-test-title-3')).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx index 8972cd2f9e..304a962212 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.test.tsx @@ -17,6 +17,7 @@ import { Entity, EntityLink } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { EntityLinksCard } from './EntityLinksCard'; @@ -43,7 +44,7 @@ describe('EntityLinksCard', () => { it('should render a link', async () => { const links: EntityLink[] = [createLink()]; - const { queryByText } = await renderWithEffects( + await renderWithEffects( wrapInTestApp( @@ -51,12 +52,12 @@ describe('EntityLinksCard', () => { ), ); - expect(queryByText('admin dashboard')).toBeInTheDocument(); - expect(queryByText('derp')).not.toBeInTheDocument(); + expect(screen.getByText('admin dashboard')).toBeInTheDocument(); + expect(screen.queryByText('derp')).not.toBeInTheDocument(); }); it('should show empty state', async () => { - const { queryByText } = await renderWithEffects( + await renderWithEffects( wrapInTestApp( @@ -65,8 +66,8 @@ describe('EntityLinksCard', () => { ); expect( - queryByText(/.*No links defined for this entity.*/), + screen.getByText(/.*No links defined for this entity.*/), ).toBeInTheDocument(); - expect(queryByText('admin dashboard')).not.toBeInTheDocument(); + expect(screen.queryByText('admin dashboard')).not.toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/EntityLinksCard/IconLink.test.tsx b/plugins/catalog/src/components/EntityLinksCard/IconLink.test.tsx index 9c1b021071..be784ae3e8 100644 --- a/plugins/catalog/src/components/EntityLinksCard/IconLink.test.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/IconLink.test.tsx @@ -17,13 +17,13 @@ import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import CloudIcon from '@material-ui/icons/Cloud'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import React from 'react'; import { IconLink } from './IconLink'; describe('IconLink', () => { it('should render an icon link', () => { - const rendered = render( + render( { , ); - expect(rendered.queryByText('I am Link')).toBeInTheDocument(); + expect(screen.getByText('I am Link')).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.test.tsx b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.test.tsx index 4f16078eb9..6f214dc1e0 100644 --- a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.test.tsx +++ b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.test.tsx @@ -16,13 +16,16 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import { EntityNotFound } from './EntityNotFound'; describe('', () => { it('renders without exploding', async () => { - const { getByText } = await renderInTestApp(); - expect(getByText(/entity was not found/i)).toBeInTheDocument(); - expect(getByText(/getting started documentation/i)).toBeInTheDocument(); - expect(getByText(/docs/i)).toBeInTheDocument(); + await renderInTestApp(); + expect(screen.getByText(/entity was not found/i)).toBeInTheDocument(); + expect( + screen.getByText(/getting started documentation/i), + ).toBeInTheDocument(); + expect(screen.getByText(/docs/i)).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx index f00474868f..6071d15166 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx @@ -15,8 +15,8 @@ */ import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react'; - import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { rootRouteRef } from '../../routes'; import { EntityOrphanWarning } from './EntityOrphanWarning'; @@ -39,7 +39,7 @@ describe('', () => { }, }; - const { getByText } = await renderInTestApp( + await renderInTestApp( ', () => { }, ); expect( - getByText( + screen.getByText( 'This entity is not referenced by any location and is therefore not receiving updates. Click here to delete.', ), ).toBeInTheDocument(); diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx index 2297493a7a..350349fe95 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx @@ -23,6 +23,7 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { EntityProcessingErrorsPanel } from './EntityProcessingErrorsPanel'; @@ -100,7 +101,7 @@ describe('', () => { rootEntityRef: stringifyEntityRef(entity), items: [{ entity, parentEntityRefs: [] }], }); - const { getByText, queryByText } = await renderInTestApp( + await renderInTestApp( @@ -109,14 +110,14 @@ describe('', () => { ); expect( - getByText( + screen.getByText( 'Error: Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object', ), ).toBeInTheDocument(); - expect(getByText('Error: Foo')).toBeInTheDocument(); - expect(queryByText('Error: This should not be rendered')).toBeNull(); + expect(screen.getByText('Error: Foo')).toBeInTheDocument(); + expect(screen.queryByText('Error: This should not be rendered')).toBeNull(); expect( - queryByText('The error below originates from'), + screen.queryByText('The error below originates from'), ).not.toBeInTheDocument(); }); @@ -204,7 +205,7 @@ describe('', () => { { entity: parent, parentEntityRefs: [] }, ], }); - const { getByText, queryByText } = await renderInTestApp( + await renderInTestApp( @@ -218,12 +219,14 @@ describe('', () => { ); expect( - getByText( + screen.getByText( 'Error: Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object', ), ).toBeInTheDocument(); - expect(getByText('Error: Foo')).toBeInTheDocument(); - expect(queryByText('Error: This should not be rendered')).toBeNull(); - expect(queryByText('The error below originates from')).toBeInTheDocument(); + expect(screen.getByText('Error: Foo')).toBeInTheDocument(); + expect(screen.queryByText('Error: This should not be rendered')).toBeNull(); + expect( + screen.getByText('The error below originates from'), + ).toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx index 4ee9d9c820..164c7e60fd 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx @@ -19,7 +19,7 @@ import { AsyncEntityProvider, EntityProvider, } from '@backstage/plugin-catalog-react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import React from 'react'; import { isKind } from './conditions'; import { EntitySwitch } from './EntitySwitch'; @@ -54,9 +54,9 @@ describe('EntitySwitch', () => { , ); - expect(rendered.queryByText('A')).toBeInTheDocument(); - expect(rendered.queryByText('B')).not.toBeInTheDocument(); - expect(rendered.queryByText('C')).not.toBeInTheDocument(); + expect(screen.getByText('A')).toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); + expect(screen.queryByText('C')).not.toBeInTheDocument(); rendered.rerender( @@ -68,9 +68,9 @@ describe('EntitySwitch', () => { , ); - expect(rendered.queryByText('A')).not.toBeInTheDocument(); - expect(rendered.queryByText('B')).toBeInTheDocument(); - expect(rendered.queryByText('C')).not.toBeInTheDocument(); + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.getByText('B')).toBeInTheDocument(); + expect(screen.queryByText('C')).not.toBeInTheDocument(); rendered.rerender( @@ -82,9 +82,9 @@ describe('EntitySwitch', () => { , ); - expect(rendered.queryByText('A')).not.toBeInTheDocument(); - expect(rendered.queryByText('B')).not.toBeInTheDocument(); - expect(rendered.queryByText('C')).toBeInTheDocument(); + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); + expect(screen.getByText('C')).toBeInTheDocument(); rendered.rerender( @@ -94,9 +94,9 @@ describe('EntitySwitch', () => { , ); - expect(rendered.queryByText('A')).not.toBeInTheDocument(); - expect(rendered.queryByText('B')).not.toBeInTheDocument(); - expect(rendered.queryByText('C')).toBeInTheDocument(); + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); + expect(screen.getByText('C')).toBeInTheDocument(); }); it('should switch child when filters switch', () => { @@ -113,8 +113,8 @@ describe('EntitySwitch', () => { , ); - expect(rendered.queryByText('A')).toBeInTheDocument(); - expect(rendered.queryByText('B')).not.toBeInTheDocument(); + expect(screen.getByText('A')).toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); rendered.rerender( @@ -127,15 +127,15 @@ describe('EntitySwitch', () => { , ); - expect(rendered.queryByText('A')).not.toBeInTheDocument(); - expect(rendered.queryByText('B')).toBeInTheDocument(); + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.getByText('B')).toBeInTheDocument(); }); it('should switch with async condition that is true', async () => { const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity; const shouldRender = () => Promise.resolve(true); - const rendered = render( + render( @@ -146,15 +146,15 @@ describe('EntitySwitch', () => { , ); - await expect(rendered.findByText('A')).resolves.toBeInTheDocument(); - expect(rendered.queryByText('B')).not.toBeInTheDocument(); + await expect(screen.findByText('A')).resolves.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); }); it('should switch with sync condition that is false', async () => { const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity; const shouldRender = () => Promise.resolve(false); - const rendered = render( + render( @@ -165,15 +165,15 @@ describe('EntitySwitch', () => { , ); - await expect(rendered.findByText('B')).resolves.toBeInTheDocument(); - expect(rendered.queryByText('A')).not.toBeInTheDocument(); + await expect(screen.findByText('B')).resolves.toBeInTheDocument(); + expect(screen.queryByText('A')).not.toBeInTheDocument(); }); it('should switch with sync condition that throws', async () => { const entity = { metadata: { name: 'mock' }, kind: 'component' } as Entity; const shouldRender = () => Promise.reject(); - const rendered = render( + render( @@ -185,8 +185,8 @@ describe('EntitySwitch', () => { , ); - await expect(rendered.findByText('C')).resolves.toBeInTheDocument(); - expect(rendered.queryByText('A')).not.toBeInTheDocument(); - expect(rendered.queryByText('B')).not.toBeInTheDocument(); + await expect(screen.findByText('C')).resolves.toBeInTheDocument(); + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); }); }); diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx index 77fdb74f2f..9d46f2ed42 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx @@ -22,7 +22,7 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import React from 'react'; import { HasComponentsCard } from './HasComponentsCard'; @@ -51,7 +51,7 @@ describe('', () => { relations: [], }; - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -64,9 +64,9 @@ describe('', () => { }, ); - expect(getByText('Has components')).toBeInTheDocument(); + expect(screen.getByText('Has components')).toBeInTheDocument(); expect( - getByText(/No component is part of this system/i), + screen.getByText(/No component is part of this system/i), ).toBeInTheDocument(); }); @@ -99,7 +99,7 @@ describe('', () => { ], }); - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -113,8 +113,8 @@ describe('', () => { ); await waitFor(() => { - expect(getByText('Has components')).toBeInTheDocument(); - expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(screen.getByText('Has components')).toBeInTheDocument(); + expect(screen.getByText(/target-name/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx index 20830d2ccd..364d503a96 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx @@ -22,7 +22,7 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import React from 'react'; import { HasResourcesCard } from './HasResourcesCard'; @@ -51,7 +51,7 @@ describe('', () => { relations: [], }; - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -59,9 +59,9 @@ describe('', () => { , ); - expect(getByText('Has resources')).toBeInTheDocument(); + expect(screen.getByText('Has resources')).toBeInTheDocument(); expect( - getByText(/No resource is part of this system/i), + screen.getByText(/No resource is part of this system/i), ).toBeInTheDocument(); }); @@ -94,7 +94,7 @@ describe('', () => { ], }); - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -108,8 +108,8 @@ describe('', () => { ); await waitFor(() => { - expect(getByText('Has resources')).toBeInTheDocument(); - expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(screen.getByText('Has resources')).toBeInTheDocument(); + expect(screen.getByText(/target-name/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx index 7fd0273939..b76a0a68ec 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx @@ -22,7 +22,7 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import React from 'react'; import { HasSubcomponentsCard } from './HasSubcomponentsCard'; @@ -51,7 +51,7 @@ describe('', () => { relations: [], }; - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -64,9 +64,9 @@ describe('', () => { }, ); - expect(getByText('Has subcomponents')).toBeInTheDocument(); + expect(screen.getByText('Has subcomponents')).toBeInTheDocument(); expect( - getByText(/No subcomponent is part of this component/i), + screen.getByText(/No subcomponent is part of this component/i), ).toBeInTheDocument(); }); @@ -99,7 +99,7 @@ describe('', () => { ], }); - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -113,8 +113,8 @@ describe('', () => { ); await waitFor(() => { - expect(getByText('Has subcomponents')).toBeInTheDocument(); - expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(screen.getByText('Has subcomponents')).toBeInTheDocument(); + expect(screen.getByText(/target-name/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx index cd11e4d3f3..634fe57e82 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx @@ -22,7 +22,7 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { waitFor } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import React from 'react'; import { HasSystemsCard } from './HasSystemsCard'; @@ -51,7 +51,7 @@ describe('', () => { relations: [], }; - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -64,8 +64,10 @@ describe('', () => { }, ); - expect(getByText('Has systems')).toBeInTheDocument(); - expect(getByText(/No system is part of this domain/i)).toBeInTheDocument(); + expect(screen.getByText('Has systems')).toBeInTheDocument(); + expect( + screen.getByText(/No system is part of this domain/i), + ).toBeInTheDocument(); }); it('shows related systems', async () => { @@ -97,7 +99,7 @@ describe('', () => { ], }); - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -111,8 +113,8 @@ describe('', () => { ); await waitFor(() => { - expect(getByText('Has systems')).toBeInTheDocument(); - expect(getByText(/target-name/i)).toBeInTheDocument(); + expect(screen.getByText('Has systems')).toBeInTheDocument(); + expect(screen.getByText(/target-name/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx index 18b45a84d4..2fcd1c2702 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx @@ -22,6 +22,7 @@ import { } from '@backstage/plugin-catalog-react'; import { Entity, RELATION_PART_OF } from '@backstage/catalog-model'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; import React from 'react'; import { SystemDiagramCard } from './SystemDiagramCard'; @@ -53,7 +54,7 @@ describe('', () => { relations: [], }; - const { queryByText } = await renderInTestApp( + await renderInTestApp( @@ -66,9 +67,9 @@ describe('', () => { }, ); - expect(queryByText(/System Diagram/)).toBeInTheDocument(); - expect(queryByText(/namespace2\/system2/)).toBeInTheDocument(); - expect(queryByText(/namespace\/entity/)).not.toBeInTheDocument(); + expect(screen.getByText(/System Diagram/)).toBeInTheDocument(); + expect(screen.getByText(/namespace2\/system2/)).toBeInTheDocument(); + expect(screen.queryByText(/namespace\/entity/)).not.toBeInTheDocument(); }); it('shows related systems', async () => { @@ -108,7 +109,7 @@ describe('', () => { ], }; - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -121,9 +122,9 @@ describe('', () => { }, ); - expect(getByText('System Diagram')).toBeInTheDocument(); - expect(getByText('namespace/system')).toBeInTheDocument(); - expect(getByText('namespace/entity')).toBeInTheDocument(); + expect(screen.getByText('System Diagram')).toBeInTheDocument(); + expect(screen.getByText('namespace/system')).toBeInTheDocument(); + expect(screen.getByText('namespace/entity')).toBeInTheDocument(); }); it('should truncate long domains, systems or entities', async () => { @@ -163,7 +164,7 @@ describe('', () => { ], }; - const { getByText } = await renderInTestApp( + await renderInTestApp( @@ -176,8 +177,8 @@ describe('', () => { }, ); - expect(getByText('namespace/alongdomai...')).toBeInTheDocument(); - expect(getByText('namespace/alongsyste...')).toBeInTheDocument(); - expect(getByText('namespace/alongentit...')).toBeInTheDocument(); + expect(screen.getByText('namespace/alongdomai...')).toBeInTheDocument(); + expect(screen.getByText('namespace/alongsyste...')).toBeInTheDocument(); + expect(screen.getByText('namespace/alongentit...')).toBeInTheDocument(); }); }); diff --git a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md index e394418c24..806d3a03a6 100644 --- a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md +++ b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-cicd-statistics-module-gitlab +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/plugin-cicd-statistics@0.1.14-next.1 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/plugin-cicd-statistics@0.1.14-next.0 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-cicd-statistics@0.1.13 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/plugin-cicd-statistics@0.1.13-next.1 + ## 0.1.7-next.0 ### Patch Changes diff --git a/plugins/cicd-statistics-module-gitlab/package.json b/plugins/cicd-statistics-module-gitlab/package.json index 5f6d5039bf..889d1c0d1f 100644 --- a/plugins/cicd-statistics-module-gitlab/package.json +++ b/plugins/cicd-statistics-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics-module-gitlab", "description": "CI/CD Statistics plugin module; Gitlab CICD", - "version": "0.1.7-next.0", + "version": "0.1.8-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md index 29852c597f..2cd087fae9 100644 --- a/plugins/cicd-statistics/CHANGELOG.md +++ b/plugins/cicd-statistics/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-cicd-statistics +## 0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## 0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + +## 0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.13-next.0 ### Patch Changes diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index 5cfac4666e..d458abf9e1 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics", "description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)", - "version": "0.1.13-next.0", + "version": "0.1.14-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index b9d916f75a..2dbd13eefd 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-circleci +## 0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.3.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.3.11 + +### Patch Changes + +- 383574c49b: Update screenshots in documentation to match latest CircleCI plugin +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index d5f36ba7d6..3419befe45 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-circleci", "description": "A Backstage plugin that integrates towards Circle CI", - "version": "0.3.11-next.0", + "version": "0.3.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -63,7 +63,7 @@ "@types/humanize-duration": "^3.25.1", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index bc83c46375..55b7650ff2 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-cloudbuild +## 0.3.12-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.3.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.3.11 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index ee2cf54177..9c6e415a65 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cloudbuild", "description": "A Backstage plugin that integrates towards Google Cloud Build", - "version": "0.3.11-next.0", + "version": "0.3.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -60,7 +60,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index aacb437e7b..a35d9b6f6c 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity } from '@backstage/catalog-model'; import { Box, diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md index 20965a33a5..51a108a2d8 100644 --- a/plugins/code-climate/CHANGELOG.md +++ b/plugins/code-climate/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-code-climate +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.11-next.0 ### Patch Changes diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json index 735c5d2c5e..2e53952aa6 100644 --- a/plugins/code-climate/package.json +++ b/plugins/code-climate/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-climate", - "version": "0.1.11-next.0", + "version": "0.1.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -49,7 +49,7 @@ "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 251cfa1b33..00085d9cf0 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/plugin-code-coverage-backend +## 0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.2.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.2.4 + +### Patch Changes + +- fcab2579a0: Adds installation instructions +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.2.4-next.1 + +### Patch Changes + +- fcab2579a0: Adds installation instructions +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + ## 0.2.4-next.0 ### Patch Changes diff --git a/plugins/code-coverage-backend/README.md b/plugins/code-coverage-backend/README.md index 4cfc187a3d..ae6021b07c 100644 --- a/plugins/code-coverage-backend/README.md +++ b/plugins/code-coverage-backend/README.md @@ -2,6 +2,68 @@ This is the backend part of the `code-coverage` plugin. It takes care of processing various coverage formats and standardizing them into a single json format, used by the frontend. +## Installation + +`yarn workspace backend add @backstage/plugin-code-coverage-backend` + +First create a `codecoverage.ts` file here: `packages/backend/src/plugins`. Now add the following as its content: + +```diff +diff --git a/packages/backend/src/plugins/codecoverage.ts b/packages/backend/src/plugins/codecoverage.ts +--- /dev/null ++++ b/packages/backend/src/plugins/codecoverage.ts +@@ -0,0 +1,15 @@ ++import { createRouter } from '@backstage/plugin-code-coverage-backend'; ++import { Router } from 'express'; ++import { PluginEnvironment } from '../types'; ++ ++export default async function createPlugin( ++ env: PluginEnvironment, ++): Promise { ++ return await createRouter({ ++ config: env.config, ++ discovery: env.discovery, ++ database: env.database, ++ urlReader: env.reader, ++ logger: env.logger, ++ }); ++} + +``` + +Finally we need to load the plugin in `packages/backend/src/index.ts`, make the following edits: + +```diff +diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts +--- a/packages/backend/src/index.ts ++++ b/packages/backend/src/index.ts +@@ -28,6 +28,7 @@ import scaffolder from './plugins/scaffolder'; + import proxy from './plugins/proxy'; + import techdocs from './plugins/techdocs'; + import search from './plugins/search'; ++import codeCoverage from './plugins/codecoverage'; + import { PluginEnvironment } from './types'; + import { ServerPermissionClient } from '@backstage/plugin-permission-node'; + import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; +@@ -85,6 +86,9 @@ async function main() { + const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); + const searchEnv = useHotMemoize(module, () => createEnv('search')); + const appEnv = useHotMemoize(module, () => createEnv('app')); ++ const codeCoverageEnv = useHotMemoize(module, () => ++ createEnv('code-coverage'), ++ ); + + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)); +@@ -93,6 +97,7 @@ async function main() { + apiRouter.use('/techdocs', await techdocs(techdocsEnv)); + apiRouter.use('/proxy', await proxy(proxyEnv)); + apiRouter.use('/search', await search(searchEnv)); ++ apiRouter.use('/code-coverage', await codeCoverage(codeCoverageEnv)); + + apiRouter.use(notFoundHandler()); +``` + ## Configuring your entity In order to use this plugin, you must set the `backstage.io/code-coverage` annotation. diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 70be7268cf..7c7e4d628a 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.2.4-next.0", + "version": "0.2.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,7 +42,7 @@ "@backstage/cli": "workspace:^", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.6", "xml2js": "^0.4.23" }, diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index 5ccbf4eed0..184d03aadb 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-code-coverage +## 0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.2.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.2.4 + +### Patch Changes + +- fcab2579a0: Adds installation instructions +- 43afded227: Updated recharts to v2.0.0 and fixed typing issues +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.2.4-next.1 + +### Patch Changes + +- fcab2579a0: Adds installation instructions +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.2.4-next.0 ### Patch Changes diff --git a/plugins/code-coverage/README.md b/plugins/code-coverage/README.md index 8903668507..ce793a78d3 100644 --- a/plugins/code-coverage/README.md +++ b/plugins/code-coverage/README.md @@ -2,6 +2,31 @@ This is the frontend part of the code-coverage plugin. It displays code coverage summaries for your entities. +## Installation + +`yarn workspace app add @backstage/plugin-code-coverage` + +Finally you need to import and render the code coverage entity, in `packages/app/src/components/catalog/EntityPage.tsx` add the following: + +```diff +@@ -70,6 +70,7 @@ import { + + import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; + import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; ++import { EntityCodeCoverageContent } from '@backstage/plugin-code-coverage'; + +@@ -226,6 +227,10 @@ const defaultEntityPage = ( + + {techdocsContent} + ++ ++ ++ ++ + + ); +``` + ## Configuring your entity In order to use this plugin, you must set the `backstage.io/code-coverage` annotation on entities for which coverage ingestion has been enabled. diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 647c7d2fb0..ca5ab9f8bd 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.2.4-next.0", + "version": "0.2.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -56,7 +56,7 @@ "@types/node": "^16.11.26", "@types/recharts": "^1.8.15", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/codescene/CHANGELOG.md b/plugins/codescene/CHANGELOG.md index d21ec8c71d..c290aabdad 100644 --- a/plugins/codescene/CHANGELOG.md +++ b/plugins/codescene/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-codescene +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 17a8e32f39: Updated dependency `rc-progress` to `3.4.1`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + ## 0.1.6-next.0 ### Patch Changes diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json index ffd297e8ed..49317fd54c 100644 --- a/plugins/codescene/package.json +++ b/plugins/codescene/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-codescene", - "version": "0.1.6-next.0", + "version": "0.1.7-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "@material-ui/core": "^4.9.10", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.57", - "rc-progress": "3.4.0", + "rc-progress": "3.4.1", "react-use": "^17.2.4" }, "peerDependencies": { @@ -47,7 +47,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 6d62429875..c7aa34cfd6 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/plugin-config-schema +## 0.1.35-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.35-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.34 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.1.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + ## 0.1.34-next.0 ### Patch Changes diff --git a/plugins/config-schema/dev/example-schema.json b/plugins/config-schema/dev/example-schema.json index bb90595340..0b96fdbe0a 100644 --- a/plugins/config-schema/dev/example-schema.json +++ b/plugins/config-schema/dev/example-schema.json @@ -19,12 +19,6 @@ "visibility": "frontend", "description": "The title of the app." }, - "googleAnalyticsTrackingId": { - "type": "string", - "visibility": "frontend", - "description": "Tracking ID for Google Analytics", - "examples": ["UA-000000-0"] - }, "listen": { "type": "object", "description": "Listening configuration for local development", @@ -665,7 +659,7 @@ "type": "object", "properties": { "rules": { - "description": "Rules to apply to all catalog entities, from any location.\n\nAn undefined list of matchers means match all, an empty list of\nmatchers means match none.\n\nThis is commonly used to put in what amounts to a whitelist of kinds\nthat regular users of Backstage are permitted to register locations\nfor. This can be used to stop them from registering yaml files\ndescribing for example a Group entity called \"admin\" that they make\nthemselves members of, or similar.", + "description": "Rules to apply to all catalog entities, from any location.\n\nAn undefined list of matchers means match all, an empty list of\nmatchers means match none.\n\nThis is commonly used to put in what amounts to an allowlist of kinds\nthat regular users of Backstage are permitted to register locations\nfor. This can be used to stop them from registering yaml files\ndescribing for example a Group entity called \"admin\" that they make\nthemselves members of, or similar.", "type": "array", "items": { "type": "object", diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index b111738405..0cfc3cbb28 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-config-schema", "description": "A Backstage plugin that lets you browse the configuration schema of your app", - "version": "0.1.34-next.0", + "version": "0.1.35-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,7 +34,7 @@ "@material-ui/lab": "4.0.0-alpha.57", "jsonschema": "^1.2.6", "react-use": "^17.2.4", - "zen-observable": "^0.8.15" + "zen-observable": "^0.10.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0" @@ -49,7 +49,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index ce062fa7e1..14f4d90565 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-cost-insights +## 0.12.1-next.1 + +### Patch Changes + +- d379b6f070: Internal refactor to avoid usage of deprecated symbols +- f97c7fd1f7: added an optional config entry `costInsights.engineerThreshold` to allow users to control the threshold value for the 'negligible' change in costs. +- a5ecdb99f0: Making a possibility to hide a trending line in a cost insights plugin +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/test-utils@1.2.3-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-cost-insights-common@0.1.1 + +## 0.12.1-next.0 + +### Patch Changes + +- f9bbb3be37: Provide the ability to change the base currency from USD to any other currency in cost insights plugin +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-cost-insights-common@0.1.1 + +## 0.12.0 + +### Minor Changes + +- 43afded227: Updated recharts to v2.0.0 and fixed typing issues + +### Patch Changes + +- e92aa15f01: Bumped `canvas` dependency to the latest version, which has better Node.js v18 support. +- cbe11d1e23: Tweak README +- 745e0e2228: Added support for displaying entity cost insights by implementing the new `getCatalogEntityDailyCost` that's part of the `CostInsightsApi`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-cost-insights-common@0.1.1 + +## 0.12.0-next.1 + +### Patch Changes + +- e92aa15f01: Bumped `canvas` dependency to the latest version, which has better Node.js v18 support. +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-cost-insights-common@0.1.1 + ## 0.12.0-next.0 ### Minor Changes diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index cc90b4d7a2..a7bcb0cb0a 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -178,6 +178,21 @@ costInsights: name: Metric C ``` +### Base Currency (Optional) + +In the case you would like to show your baseline costs on the graph on other currency than US dollars. + +```yaml +## ./app-config.yaml +costInsights: + engineerCost: 200000 + baseCurrency: + locale: nl-NL + options: + currency: EUR + minimumFractionDigits: 3 +``` + ### Currencies (Optional) In the `Cost Overview` panel, users can choose from a dropdown of currencies to see costs in, such as Engineers or USD. Currencies must be defined as keys on the `currencies` field. A user-friendly label and unit are **required**. If not set, the `defaultCurrencies` in `currency.ts` will be used. @@ -207,6 +222,19 @@ costInsights: rate: 3.5 ``` +### Engineer Threshold (Optional; default 0.5) + +This threshold determines whether to show 'Negligible', or a percentage with a fraction of 'engineers' for cost savings or cost excess on top of the charts. +A threshold of 0.5 means that `Negligible` is shown when the difference in costs is lower than that fraction of engineers in that time frame, +and show `XX% or ~N engineers` when it's above the threshold. + +```yaml +## ./app-config.yaml +costInsights: + engineerCost: 200000 + engineerThreshold: 0.5 +``` + ## Alerts The CostInsightsApi `getAlerts` method may return any type of alert or recommendation (called collectively "Action Items" in Cost Insights) that implements the [Alert type](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/types/Alert.ts). This allows you to deliver any alerts or recommendations specific to your infrastructure or company migrations. diff --git a/plugins/cost-insights/api-report.md b/plugins/cost-insights/api-report.md index f50f0b0c81..00cbc4c3af 100644 --- a/plugins/cost-insights/api-report.md +++ b/plugins/cost-insights/api-report.md @@ -11,10 +11,17 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import { ChangeStatistic as ChangeStatistic_2 } from '@backstage/plugin-cost-insights-common'; import * as common from '@backstage/plugin-cost-insights-common'; +import { Cost as Cost_2 } from '@backstage/plugin-cost-insights-common'; import { Dispatch } from 'react'; +import { Entity as Entity_2 } from '@backstage/plugin-cost-insights-common'; import { ForwardRefExoticComponent } from 'react'; +import { Group as Group_2 } from '@backstage/plugin-cost-insights-common'; import { Maybe as Maybe_2 } from '@backstage/plugin-cost-insights-common'; +import { Metric as Metric_2 } from '@backstage/plugin-cost-insights-common'; +import { MetricData as MetricData_2 } from '@backstage/plugin-cost-insights-common'; import { PaletteOptions } from '@material-ui/core/styles/createPalette'; +import { Product as Product_2 } from '@backstage/plugin-cost-insights-common'; +import { Project as Project_2 } from '@backstage/plugin-cost-insights-common'; import { PropsWithChildren } from 'react'; import { ReactNode } from 'react'; import { RefAttributes } from 'react'; @@ -227,10 +234,12 @@ export type ChartData = { // @public (undocumented) export type ConfigContextProps = { - metrics: Metric[]; - products: Product[]; + baseCurrency: Intl.NumberFormat; + metrics: Metric_2[]; + products: Product_2[]; icons: Icon[]; engineerCost: number; + engineerThreshold: number; currencies: Currency[]; }; @@ -247,30 +256,34 @@ export const CostGrowthIndicator: ( // @public (undocumented) export type CostGrowthIndicatorProps = TypographyProps & { - change: ChangeStatistic; + change: ChangeStatistic_2; formatter?: ( - change: ChangeStatistic, + change: ChangeStatistic_2, options?: { absolute: boolean; }, - ) => Maybe; + ) => Maybe_2; }; // @public (undocumented) export type CostGrowthProps = { - change: ChangeStatistic; + change: ChangeStatistic_2; duration: Duration; }; // @public (undocumented) export type CostInsightsApi = { getLastCompleteBillingDate(): Promise; - getUserGroups(userId: string): Promise; - getGroupProjects(group: string): Promise; - getGroupDailyCost(group: string, intervals: string): Promise; - getProjectDailyCost(project: string, intervals: string): Promise; - getDailyMetricData(metric: string, intervals: string): Promise; - getProductInsights(options: ProductInsightsOptions): Promise; + getUserGroups(userId: string): Promise; + getGroupProjects(group: string): Promise; + getCatalogEntityDailyCost?( + catalogEntityRef: string, + intervals: string, + ): Promise; + getGroupDailyCost(group: string, intervals: string): Promise; + getProjectDailyCost(project: string, intervals: string): Promise; + getDailyMetricData(metric: string, intervals: string): Promise; + getProductInsights(options: ProductInsightsOptions): Promise; getAlerts(group: string): Promise; }; @@ -312,7 +325,7 @@ const costInsightsPlugin: BackstagePlugin< unlabeledDataflowAlerts: RouteRef; }, {}, - {} + CostInsightsInputPluginOptions >; export { costInsightsPlugin }; export { costInsightsPlugin as plugin }; @@ -404,24 +417,32 @@ export const EngineerThreshold = 0.5; // @public @deprecated (undocumented) export type Entity = common.Entity; +// @public +export const EntityCostInsightsContent: () => JSX.Element; + // @public (undocumented) export class ExampleCostInsightsClient implements CostInsightsApi { // (undocumented) getAlerts(group: string): Promise; // (undocumented) - getDailyMetricData(metric: string, intervals: string): Promise; + getCatalogEntityDailyCost( + entityRef: string, + intervals: string, + ): Promise; // (undocumented) - getGroupDailyCost(group: string, intervals: string): Promise; + getDailyMetricData(metric: string, intervals: string): Promise; // (undocumented) - getGroupProjects(group: string): Promise; + getGroupDailyCost(group: string, intervals: string): Promise; + // (undocumented) + getGroupProjects(group: string): Promise; // (undocumented) getLastCompleteBillingDate(): Promise; // (undocumented) - getProductInsights(options: ProductInsightsOptions): Promise; + getProductInsights(options: ProductInsightsOptions): Promise; // (undocumented) - getProjectDailyCost(project: string, intervals: string): Promise; + getProjectDailyCost(project: string, intervals: string): Promise; // (undocumented) - getUserGroups(userId: string): Promise; + getUserGroups(userId: string): Promise; } // @public @deprecated (undocumented) @@ -526,7 +547,7 @@ export type ProductInsightsOptions = { product: string; group: string; intervals: string; - project: Maybe; + project: Maybe_2; }; // @public (undocumented) diff --git a/plugins/cost-insights/config.d.ts b/plugins/cost-insights/config.d.ts index 4144b6d6a5..61a54f76e2 100644 --- a/plugins/cost-insights/config.d.ts +++ b/plugins/cost-insights/config.d.ts @@ -21,6 +21,63 @@ export interface Config { */ engineerCost: number; + /** + * @visibility frontend + */ + engineerThreshold?: number; + + /** + * @visibility frontend + */ + baseCurrency?: { + /** + * @visibility frontend + */ + locale?: string; + options?: { + /** + * @visibility frontend + */ + localeMatcher?: string | undefined; + /** + * @visibility frontend + */ + style?: string | undefined; + /** + * @visibility frontend + */ + currency?: string | undefined; + /** + * @visibility frontend + */ + currencySign?: string | undefined; + /** + * @visibility frontend + */ + useGrouping?: boolean | undefined; + /** + * @visibility frontend + */ + minimumIntegerDigits?: number | undefined; + /** + * @visibility frontend + */ + minimumFractionDigits?: number | undefined; + /** + * @visibility frontend + */ + maximumFractionDigits?: number | undefined; + /** + * @visibility frontend + */ + minimumSignificantDigits?: number | undefined; + /** + * @visibility frontend + */ + maximumSignificantDigits?: number | undefined; + }; + }; + products?: { [kind: string]: { /** diff --git a/plugins/cost-insights/dev/index.tsx b/plugins/cost-insights/dev/index.tsx index 4be788427a..622abaffc9 100644 --- a/plugins/cost-insights/dev/index.tsx +++ b/plugins/cost-insights/dev/index.tsx @@ -23,7 +23,26 @@ import { CostInsightsPage, CostInsightsProjectGrowthInstructionsPage, CostInsightsLabelDataflowInstructionsPage, + EntityCostInsightsContent, } from '../src/plugin'; +import { Content, Header, Page } from '@backstage/core-components'; +import { Grid } from '@material-ui/core'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { Entity } from '@backstage/catalog-model'; + +const mockEntity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'backstage', + description: 'backstage.io', + }, + spec: { + lifecycle: 'production', + type: 'service', + owner: 'user:guest', + }, +}; createDevApp() .registerPlugin(costInsightsPlugin) @@ -44,4 +63,22 @@ createDevApp() title: 'Labelling', element: , }) + .addPage({ + title: 'Entity', + element: ( + +
+ + + + + + + + + + + + ), + }) .render(); diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index b5859fa886..8ba7b25a8f 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cost-insights", "description": "A Backstage plugin that helps you keep track of your cloud spend", - "version": "0.12.0-next.0", + "version": "0.12.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,7 +36,9 @@ "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-cost-insights-common": "workspace:^", + "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -71,9 +73,9 @@ "@types/recharts": "^1.8.14", "@types/regression": "^2.0.0", "@types/yup": "^0.29.13", - "canvas": "^2.6.1", + "canvas": "^2.10.2", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index 3c97407f08..959b40e584 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -15,14 +15,15 @@ */ import { - Alert, Cost, Entity, Group, Project, Maybe, MetricData, -} from '../types'; +} from '@backstage/plugin-cost-insights-common'; + +import { Alert } from '../types'; import { createApiRef } from '@backstage/core-plugin-api'; /** @public */ @@ -77,6 +78,29 @@ export type CostInsightsApi = { */ getGroupProjects(group: string): Promise; + /** + * Get daily cost aggregations for a given catalog entity and interval time frame. + * + * The return type includes an array of daily cost aggregations as well as statistics about the + * change in cost over the intervals. Calculating these statistics requires us to bucket costs + * into two or more time periods, hence a repeating interval format rather than just a start and + * end date. + * + * The rate of change in this comparison allows teams to reason about their cost growth (or + * reduction) and compare it to metrics important to the business. + * + * Note: implementing this is only required when using the `EntityCostInsightsContent` extension. + * + * @param catalogEntityRef - A reference to the catalog entity, as described in + * https://backstage.io/docs/features/software-catalog/references + * @param intervals - An ISO 8601 repeating interval string, such as R2/P30D/2020-09-01 + * https://en.wikipedia.org/wiki/ISO_8601#Repeating_intervals + */ + getCatalogEntityDailyCost?( + catalogEntityRef: string, + intervals: string, + ): Promise; + /** * Get daily cost aggregations for a given group and interval time frame. * diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx index af976c6d3f..385116e188 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertDialog.tsx @@ -28,7 +28,8 @@ import { } from '@material-ui/core'; import { default as CloseIcon } from '@material-ui/icons/Close'; import { useAlertDialogStyles as useStyles } from '../../utils/styles'; -import { Alert, AlertStatus, Maybe } from '../../types'; +import { Alert, AlertStatus } from '../../types'; +import { Maybe } from '@backstage/plugin-cost-insights-common'; import { choose, formOf } from '../../utils/alerts'; const DEFAULT_FORM_ID = 'alert-form'; diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx index 2daacda85e..e48ae2789d 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx @@ -30,7 +30,8 @@ import { MapLoadingToProps, } from '../../hooks'; import { DefaultLoadingAction } from '../../utils/loading'; -import { Alert, AlertOptions, AlertStatus, Maybe } from '../../types'; +import { Alert, AlertOptions, AlertStatus } from '../../types'; +import { Maybe } from '@backstage/plugin-cost-insights-common'; import { isStatusSnoozed, isStatusAccepted, diff --git a/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx b/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx index 4e3ca9d78c..37204d5ec6 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChart.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import { BarChart, BarChartProps } from './BarChart'; import { ResourceData } from '../../types'; -import { createMockEntity } from '../../testUtils'; +import { createMockEntity, MockConfigProvider } from '../../testUtils'; import { resourceSort } from '../../utils/sort'; import { renderInTestApp } from '@backstage/test-utils'; @@ -46,11 +46,13 @@ const renderWithProps = ({ resources = MockResources, }: BarChartProps) => { return renderInTestApp( - , + + + , ); }; @@ -102,7 +104,7 @@ describe('', () => { it('should display stepper if displaying more than 6 resources', async () => { const rendered = await renderWithProps({} as BarChartProps); - expect(rendered.queryByTestId('bar-chart-stepper')).toBeInTheDocument(); + expect(rendered.getByTestId('bar-chart-stepper')).toBeInTheDocument(); }); it('should display the next step button if resources are remaining', async () => { @@ -112,7 +114,7 @@ describe('', () => { rendered.queryByTestId('bar-chart-stepper-button-back'), ).not.toBeInTheDocument(); expect( - rendered.queryByTestId('bar-chart-stepper-button-next'), + rendered.getByTestId('bar-chart-stepper-button-next'), ).toBeInTheDocument(); }); diff --git a/plugins/cost-insights/src/components/BarChart/BarChart.tsx b/plugins/cost-insights/src/components/BarChart/BarChart.tsx index 4f4dea8ddd..fa4ae18e24 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChart.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChart.tsx @@ -40,20 +40,24 @@ import { notEmpty } from '../../utils/assert'; import { useBarChartStyles } from '../../utils/styles'; import { resourceSort } from '../../utils/sort'; import { isInvalid, titleOf, tooltipItemOf } from '../../utils/graphs'; -import { TooltipRenderer } from '../../types/Tooltip'; +import { TooltipRenderer } from '../../types'; +import { useConfig } from '../../hooks'; -export const defaultTooltip: TooltipRenderer = ({ label, payload = [] }) => { - if (isInvalid({ label, payload })) return null; +const defaultTooltip = (baseCurrency: Intl.NumberFormat) => { + const tooltip: TooltipRenderer = ({ label, payload = [] }) => { + if (isInvalid({ label, payload })) return null; - const title = titleOf(label); - const items = payload.map(tooltipItemOf).filter(notEmpty); - return ( - - {items.map((item, index) => ( - - ))} - - ); + const title = titleOf(label); + const items = payload.map(tooltipItemOf(baseCurrency)).filter(notEmpty); + return ( + + {items.map((item, index) => ( + + ))} + + ); + }; + return tooltip; }; /** @public */ @@ -69,12 +73,14 @@ export type BarChartProps = { /** @public */ export const BarChart = (props: BarChartProps) => { + const { baseCurrency } = useConfig(); + const { resources, responsive = true, displayAmount = 6, options = {}, - tooltip = defaultTooltip, + tooltip = defaultTooltip(baseCurrency), onClick, onMouseMove, } = props; @@ -164,7 +170,7 @@ export const BarChart = (props: BarChartProps) => { tick={BarChartTick} /> 0, globalResourcesMax]} tick={styles.axis} /> diff --git a/plugins/cost-insights/src/components/BarChart/BarChartLegend.test.tsx b/plugins/cost-insights/src/components/BarChart/BarChartLegend.test.tsx index 07ff144862..db515c233d 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChartLegend.test.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChartLegend.test.tsx @@ -17,13 +17,16 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { BarChartLegend } from './BarChartLegend'; +import { MockConfigProvider } from '../../testUtils'; describe('', () => { it(`Should display the correct cost start and end`, async () => { const rendered = await renderInTestApp( - , + + , + , ); expect(rendered.getByText(/\$1,000/)).toBeInTheDocument(); - expect(rendered.queryByText(/\$5,000/)).toBeInTheDocument(); + expect(rendered.getByText(/\$5,000/)).toBeInTheDocument(); }); }); diff --git a/plugins/cost-insights/src/components/BarChart/BarChartLegend.tsx b/plugins/cost-insights/src/components/BarChart/BarChartLegend.tsx index 72c3120dd4..becd5467d1 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChartLegend.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChartLegend.tsx @@ -20,6 +20,7 @@ import { LegendItem } from '../LegendItem'; import { currencyFormatter } from '../../utils/formatters'; import { CostInsightsTheme } from '../../types'; import { useBarChartLayoutStyles as useStyles } from '../../utils/styles'; +import { useConfig } from '../../hooks'; /** @public */ export type BarChartLegendOptions = { @@ -45,6 +46,7 @@ export const BarChartLegend = ( const theme = useTheme(); const classes = useStyles(); + const { baseCurrency } = useConfig(); const data = Object.assign( { @@ -63,7 +65,7 @@ export const BarChartLegend = ( title={data.previousName} markerColor={options.hideMarker ? undefined : data.previousFill} > - {currencyFormatter.format(costStart)} + {currencyFormatter(baseCurrency).format(costStart)} @@ -71,7 +73,7 @@ export const BarChartLegend = ( title={data.currentName} markerColor={options.hideMarker ? undefined : data.currentFill} > - {currencyFormatter.format(costEnd)} + {currencyFormatter(baseCurrency).format(costEnd)} {children} diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx index 5f47544f7b..3da7af90fb 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx @@ -17,7 +17,7 @@ import React, { PropsWithChildren } from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { CostGrowth } from './CostGrowth'; -import { Currency, CurrencyType, Duration } from '../../types'; +import { ChangeThreshold, Currency, CurrencyType, Duration } from '../../types'; import { findAlways } from '../../utils/assert'; import { MockConfigProvider, MockCurrencyProvider } from '../../testUtils'; import { defaultCurrencies } from '../../utils/currency'; @@ -33,11 +33,16 @@ const MockContext = ({ children, currency, engineerCost, + engineerThreshold, }: PropsWithChildren<{ currency: Currency; engineerCost: number; + engineerThreshold?: number; }>) => ( - + {children} ); @@ -104,3 +109,30 @@ describe.each` expect(getByText(expected)).toBeInTheDocument(); }); }); + +describe.each` + ratio | amount | threshold | expected + ${0} | ${0} | ${0} | ${'less than an engineer'} + ${0} | ${0} | ${200} | ${'Negligible'} + ${ChangeThreshold.lower} | ${0.5} | ${0} | ${'less than an engineer'} + ${ChangeThreshold.lower} | ${0.5} | ${0.5} | ${'Negligible'} + ${ChangeThreshold.upper} | ${0.5} | ${0.000001} | ${'less than an engineer'} + ${ChangeThreshold.upper} | ${0.5} | ${0.5} | ${'Negligible'} + ${3} | ${500_000} | ${0} | ${`300% or ~30 engineers`} + ${3} | ${500_000} | ${0.5} | ${`300% or ~30 engineers`} + ${3} | ${500_000} | ${29} | ${'300% or ~30 engineers'} + ${3} | ${500_000} | ${30} | ${'Negligible'} +`('', ({ ratio, amount, threshold, expected }) => { + it(`should display the correct difference the threshold is different. ratio: ${ratio} threshold:${threshold} expected:${expected}`, async () => { + const { getByText } = await renderInTestApp( + + + , + ); + expect(getByText(expected)).toBeInTheDocument(); + }); +}); diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx index ffc548c7ad..51f9c61e7b 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.tsx @@ -16,13 +16,8 @@ import React from 'react'; import classnames from 'classnames'; -import { - ChangeStatistic, - CurrencyType, - Duration, - EngineerThreshold, - GrowthType, -} from '../../types'; +import { CurrencyType, Duration, GrowthType } from '../../types'; +import { ChangeStatistic } from '@backstage/plugin-cost-insights-common'; import { rateOf } from '../../utils/currency'; import { growthOf } from '../../utils/change'; import { useCostGrowthStyles as useStyles } from '../../utils/styles'; @@ -42,7 +37,7 @@ export const CostGrowth = (props: CostGrowthProps) => { const { change, duration } = props; const styles = useStyles(); - const { engineerCost } = useConfig(); + const { engineerCost, engineerThreshold } = useConfig(); const [currency] = useCurrency(); // Only display costs in absolute values @@ -55,7 +50,7 @@ export const CostGrowth = (props: CostGrowthProps) => { // If a ratio cannot be calculated, don't format. const growth = notEmpty(change.ratio) - ? growthOf({ ratio: change.ratio, amount: engineers }) + ? growthOf({ ratio: change.ratio, amount: engineers }, engineerThreshold) : null; // Determine if growth is significant enough to highlight const classes = classnames({ @@ -63,7 +58,7 @@ export const CostGrowth = (props: CostGrowthProps) => { [styles.savings]: growth === GrowthType.Savings, }); - if (engineers < EngineerThreshold) { + if (engineers < engineerThreshold) { return Negligible; } diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.test.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.test.tsx index d0e7d3ff74..a83a520db2 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.test.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.test.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { renderInTestApp } from '@backstage/test-utils'; import { CostGrowthIndicator } from './CostGrowthIndicator'; import { ChangeThreshold, EngineerThreshold } from '../../types'; +import { MockConfigProvider } from '../../testUtils'; describe.each` ratio | amount | ariaLabel @@ -30,7 +31,9 @@ describe.each` `('growthOf', ({ ratio, amount, ariaLabel }) => { it(`should display the correct indicator for ${ariaLabel}`, async () => { const { getByLabelText } = await renderInTestApp( - , + + + , ); expect(getByLabelText(ariaLabel)).toBeInTheDocument(); }); @@ -48,7 +51,9 @@ describe.each` `('growthOf', ({ ratio, amount }) => { it('should display the correct indicator for negligible growth', async () => { const { queryByLabelText } = await renderInTestApp( - , + + + , ); expect(queryByLabelText('savings')).not.toBeInTheDocument(); expect(queryByLabelText('excess')).not.toBeInTheDocument(); diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.tsx index 8993504c84..cf07d97d0a 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowthIndicator.tsx @@ -20,8 +20,10 @@ import { Typography, TypographyProps } from '@material-ui/core'; import { default as ArrowDropUp } from '@material-ui/icons/ArrowDropUp'; import { default as ArrowDropDown } from '@material-ui/icons/ArrowDropDown'; import { growthOf } from '../../utils/change'; -import { ChangeStatistic, GrowthType, Maybe } from '../../types'; +import { GrowthType } from '../../types'; import { useCostGrowthStyles as useStyles } from '../../utils/styles'; +import { ChangeStatistic, Maybe } from '@backstage/plugin-cost-insights-common'; +import { useConfig } from '../../hooks'; /** @public */ export type CostGrowthIndicatorProps = TypographyProps & { @@ -34,10 +36,12 @@ export type CostGrowthIndicatorProps = TypographyProps & { /** @public */ export const CostGrowthIndicator = (props: CostGrowthIndicatorProps) => { + const { engineerThreshold } = useConfig(); const { change, formatter, className, ...extraProps } = props; const classes = useStyles(); - const growth = growthOf(change); + + const growth = growthOf(change, engineerThreshold); const classNames = classnames(classes.indicator, className, { [classes.excess]: growth === GrowthType.Excess, diff --git a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx index 015a6279da..337d3d06b2 100644 --- a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx @@ -17,7 +17,6 @@ import { CostInsightsHeader } from './CostInsightsHeader'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; - import { ApiProvider } from '@backstage/core-app-api'; import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; @@ -43,7 +42,7 @@ describe('', () => { , ); - expect(rendered.queryByText(/doing great/)).toBeInTheDocument(); + expect(rendered.getByText(/doing great/)).toBeInTheDocument(); }); it('Shows work to do when alerts > 1', async () => { @@ -57,7 +56,7 @@ describe('', () => { /> , ); - expect(rendered.queryByText(/few things/)).toBeInTheDocument(); + expect(rendered.getByText(/few things/)).toBeInTheDocument(); }); it('Handles grammar with a single alert', async () => { @@ -73,7 +72,7 @@ describe('', () => { ); expect(rendered.queryByText(/things/)).not.toBeInTheDocument(); - expect(rendered.queryByText(/one thing/)).toBeInTheDocument(); + expect(rendered.getByText(/one thing/)).toBeInTheDocument(); }); it('Shows no costs when hasCostData is false', async () => { @@ -87,7 +86,7 @@ describe('', () => { /> , ); - expect(rendered.queryByText(/this is awkward/)).toBeInTheDocument(); + expect(rendered.getByText(/this is awkward/)).toBeInTheDocument(); }); describe.each` @@ -109,9 +108,7 @@ describe('', () => { /> , ); - expect( - rendered.queryByText(/Test group display name/), - ).toBeInTheDocument(); + expect(rendered.getByText(/Test group display name/)).toBeInTheDocument(); }); it('Fallbacks to group id when display name not available', async () => { @@ -125,7 +122,7 @@ describe('', () => { /> , ); - expect(rendered.queryByText(/test-user-group-1/)).toBeInTheDocument(); + expect(rendered.getByText(/test-user-group-1/)).toBeInTheDocument(); }); }); }); diff --git a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.tsx b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.tsx index 540165d1d9..8e12b7f5dd 100644 --- a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.tsx +++ b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { Typography } from '@material-ui/core'; import useAsync from 'react-use/lib/useAsync'; import { useCostInsightsStyles } from '../../utils/styles'; -import { Group } from '../../types'; +import { Group } from '@backstage/plugin-cost-insights-common'; import { identityApiRef, useApi } from '@backstage/core-plugin-api'; function useDisplayName(): string { diff --git a/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx b/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx index 93614bd64f..0a583aae0d 100644 --- a/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx +++ b/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx @@ -15,7 +15,7 @@ */ import React, { PropsWithChildren } from 'react'; import { makeStyles } from '@material-ui/core'; -import { Group } from '../../types'; +import { Group } from '@backstage/plugin-cost-insights-common'; import { CostInsightsTabs } from '../CostInsightsTabs'; import { Header, Page } from '@backstage/core-components'; diff --git a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx index 8ccca96e0f..4cfabc119c 100644 --- a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx @@ -18,7 +18,8 @@ import React from 'react'; import { default as HappyFace } from '@material-ui/icons/SentimentSatisfiedAlt'; import { renderInTestApp } from '@backstage/test-utils'; import { CostInsightsNavigation } from './CostInsightsNavigation'; -import { Product, Icon } from '../../types'; +import { Icon } from '../../types'; +import { Product } from '@backstage/plugin-cost-insights-common'; import { MockConfigProvider, MockScrollProvider } from '../../testUtils'; import { getDefaultNavigationItems } from '../../utils/navigation'; diff --git a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx index ad7dea55fd..507016f391 100644 --- a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx +++ b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.tsx @@ -32,7 +32,7 @@ import { NavigationItem, getDefaultNavigationItems, } from '../../utils/navigation'; -import { Maybe, Product } from '../../types'; +import { Maybe, Product } from '@backstage/plugin-cost-insights-common'; type CostInsightsNavigationProps = { alerts: number; diff --git a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx index 00d92f945d..d113f1b8f3 100644 --- a/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx +++ b/plugins/cost-insights/src/components/CostInsightsPage/CostInsightsPage.tsx @@ -48,7 +48,14 @@ import { useLastCompleteBillingDate, useLoading, } from '../../hooks'; -import { Alert, Cost, Maybe, MetricData, Product, Project } from '../../types'; +import { Alert } from '../../types'; +import { + Cost, + Maybe, + MetricData, + Product, + Project, +} from '@backstage/plugin-cost-insights-common'; import { mapLoadingToProps } from './selector'; import { ProjectSelect } from '../ProjectSelect'; import { intervalsOf } from '../../utils/duration'; diff --git a/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx b/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx index 2e2131daff..b04f314e73 100644 --- a/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { CostInsightsTabs } from './CostInsightsTabs'; import userEvent from '@testing-library/user-event'; -import { Group } from '../../types'; +import { Group } from '@backstage/plugin-cost-insights-common'; import { MockFilterProvider, MockLoadingProvider } from '../../testUtils'; import { renderInTestApp } from '@backstage/test-utils'; diff --git a/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.tsx b/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.tsx index 935184ee55..152ce56902 100644 --- a/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.tsx +++ b/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.tsx @@ -18,7 +18,7 @@ import React, { useState } from 'react'; import { Menu, MenuItem, Tab, Tabs, Typography } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { mapLoadingToProps, mapFiltersToProps } from './selector'; -import { Group } from '../../types'; +import { Group } from '@backstage/plugin-cost-insights-common'; import { useFilters, useLoading } from '../../hooks'; import { useCostInsightsTabsStyles as useStyles } from '../../utils/styles'; diff --git a/plugins/cost-insights/src/components/CostInsightsTabs/selector.ts b/plugins/cost-insights/src/components/CostInsightsTabs/selector.ts index 6309016c85..7e19deb301 100644 --- a/plugins/cost-insights/src/components/CostInsightsTabs/selector.ts +++ b/plugins/cost-insights/src/components/CostInsightsTabs/selector.ts @@ -16,7 +16,8 @@ import { MapFiltersToProps } from '../../hooks/useFilters'; import { MapLoadingToProps } from '../../hooks/useLoading'; -import { Group, PageFilters } from '../../types'; +import { PageFilters } from '../../types'; +import { Group } from '@backstage/plugin-cost-insights-common'; import { getResetStateWithoutInitial } from '../../utils/loading'; type CostInsightsTabsFilterProps = PageFilters & { diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx index f6463b65ed..842e76c991 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewBreakdownChart.tsx @@ -16,53 +16,57 @@ import React, { useState } from 'react'; import { DateTime } from 'luxon'; import { - useTheme, Box, - Typography, Divider, emphasize, + Typography, + useTheme, } from '@material-ui/core'; import { default as FullScreenIcon } from '@material-ui/icons/Fullscreen'; import { + Area, AreaChart, + CartesianGrid, + ResponsiveContainer, + Tooltip as RechartsTooltip, XAxis, YAxis, - Tooltip as RechartsTooltip, - Area, - ResponsiveContainer, - CartesianGrid, } from 'recharts'; -import { Cost, DEFAULT_DATE_FORMAT, CostInsightsTheme } from '../../types'; +import { DEFAULT_DATE_FORMAT, CostInsightsTheme } from '../../types'; +import { Cost } from '@backstage/plugin-cost-insights-common'; import { + BarChartLegend, BarChartTooltip as Tooltip, BarChartTooltipItem as TooltipItem, - BarChartLegend, } from '../BarChart'; import { - overviewGraphTickFormatter, formatGraphValue, isInvalid, + overviewGraphTickFormatter, } from '../../utils/graphs'; import { useCostOverviewStyles as useStyles } from '../../utils/styles'; -import { useFilters, useLastCompleteBillingDate } from '../../hooks'; +import { useConfig, useFilters, useLastCompleteBillingDate } from '../../hooks'; import { mapFiltersToProps } from './selector'; import { getPreviousPeriodTotalCost } from '../../utils/change'; import { formatPeriod } from '../../utils/formatters'; import { aggregationSum } from '../../utils/sum'; -import { BarChartLegendOptions } from '../BarChart/BarChartLegend'; -import { TooltipRenderer } from '../../types/Tooltip'; +import { BarChartLegendOptions } from '../BarChart'; +import { TooltipRenderer } from '../../types'; export type CostOverviewBreakdownChartProps = { costBreakdown: Cost[]; + responsive?: boolean; }; const LOW_COST_THRESHOLD = 0.1; export const CostOverviewBreakdownChart = ({ costBreakdown, + responsive = true, }: CostOverviewBreakdownChartProps) => { const theme = useTheme(); const classes = useStyles(theme); + const { baseCurrency } = useConfig(); const lastCompleteBillingDate = useLastCompleteBillingDate(); const { duration } = useFilters(mapFiltersToProps); const [isExpanded, setExpanded] = useState(false); @@ -183,9 +187,10 @@ export const CostOverviewBreakdownChart = ({ ? DateTime.fromMillis(label) : DateTime.fromISO(label!); const dateTitle = date.toUTC().toFormat(DEFAULT_DATE_FORMAT); + const formatGraphValueWith = formatGraphValue(baseCurrency); const items = payload.map((p, i) => ({ label: p.dataKey as string, - value: formatGraphValue(Number(p.value), i), + value: formatGraphValueWith(Number(p.value), i), fill: p.color!, })); const expandText = ( @@ -228,7 +233,7 @@ export const CostOverviewBreakdownChart = ({ /> 0, 'dataMax']} tick={{ fill: classes.axis.fill }} - tickFormatter={formatGraphValue} + tickFormatter={formatGraphValue(baseCurrency)} width={classes.yAxis.width} /> {renderAreas()} diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx index e3de9593d9..0a9451c794 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.test.tsx @@ -15,19 +15,19 @@ */ import React from 'react'; import { fireEvent } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { MockPluginProvider, renderInTestApp } from '@backstage/test-utils'; import { CostOverviewCard } from './CostOverviewCard'; -import { Cost } from '../../types'; +import { Cost } from '@backstage/plugin-cost-insights-common'; import { changeOf, getGroupedProducts, getGroupedProjects, MockAggregatedDailyCosts, - trendlineOf, MockBillingDateProvider, MockConfigProvider, MockFilterProvider, MockScrollProvider, + trendlineOf, } from '../../testUtils'; import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider'; @@ -44,7 +44,9 @@ function renderInContext(children: JSX.Element) { - {children} + + {children} + diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx index 3e77858967..c816da97ed 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewCard.tsx @@ -34,7 +34,12 @@ import { useConfig, useFilters } from '../../hooks'; import { mapFiltersToProps } from './selector'; import { DefaultNavigation } from '../../utils/navigation'; import { findAlways } from '../../utils/assert'; -import { Cost, CostInsightsTheme, Maybe, MetricData } from '../../types'; +import { CostInsightsTheme } from '../../types'; +import { + Cost, + Maybe, + MetricData, +} from '@backstage/plugin-cost-insights-common'; import { useOverviewTabsStyles } from '../../utils/styles'; import { ScrollAnchor } from '../../utils/scroll'; @@ -103,7 +108,8 @@ export const CostOverviewCard = ({ }; // Metrics can only be selected on the total cost graph - const showMetricSelect = config.metrics.length && safeTabIndex === 0; + const showMetricSelect = + metricData && config.metrics.length && safeTabIndex === 0; return ( diff --git a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx index 1c58bf58e6..c46f2b0b82 100644 --- a/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx +++ b/plugins/cost-insights/src/components/CostOverviewCard/CostOverviewChart.tsx @@ -26,15 +26,13 @@ import { Line, ResponsiveContainer, } from 'recharts'; +import { ChartData, DEFAULT_DATE_FORMAT, CostInsightsTheme } from '../../types'; import { - ChartData, Cost, - DEFAULT_DATE_FORMAT, Maybe, Metric, MetricData, - CostInsightsTheme, -} from '../../types'; +} from '@backstage/plugin-cost-insights-common'; import { BarChartTooltip as Tooltip, BarChartTooltipItem as TooltipItem, @@ -48,7 +46,9 @@ import { useCostOverviewStyles as useStyles } from '../../utils/styles'; import { groupByDate, toDataMax, trendFrom } from '../../utils/charts'; import { aggregationSort } from '../../utils/sort'; import { CostOverviewLegend } from './CostOverviewLegend'; -import { TooltipRenderer } from '../../types/Tooltip'; +import { TooltipRenderer } from '../../types'; +import { useCostInsightsOptions } from '../../options'; +import { useConfig } from '../../hooks'; type CostOverviewChartProps = { metric: Maybe; @@ -65,6 +65,7 @@ export const CostOverviewChart = ({ }: CostOverviewChartProps) => { const theme = useTheme(); const styles = useStyles(theme); + const { baseCurrency } = useConfig(); const data = { dailyCost: { @@ -106,6 +107,7 @@ export const CostOverviewChart = ({ ? DateTime.fromMillis(label) : DateTime.fromISO(label!); const title = date.toUTC().toFormat(DEFAULT_DATE_FORMAT); + const formatGraphValueWith = formatGraphValue(baseCurrency); const items = payload .filter(p => dataKeys.includes(p.dataKey as string)) .map((p, i) => ({ @@ -115,8 +117,8 @@ export const CostOverviewChart = ({ : data.metric.name, value: p.dataKey === data.dailyCost.dataKey - ? formatGraphValue(Number(p.value), i, data.dailyCost.format) - : formatGraphValue(Number(p.value), i, data.metric.format), + ? formatGraphValueWith(Number(p.value), i, data.dailyCost.format) + : formatGraphValueWith(Number(p.value), i, data.metric.format), fill: p.dataKey === data.dailyCost.dataKey ? theme.palette.blue @@ -132,6 +134,8 @@ export const CostOverviewChart = ({ ); }; + const { hideTrendLine } = useCostInsightsOptions(); + return ( 0, 'dataMax']} tick={{ fill: styles.axis.fill }} - tickFormatter={formatGraphValue} + tickFormatter={formatGraphValue(baseCurrency)} width={styles.yAxis.width} yAxisId={data.dailyCost.dataKey} /> @@ -177,15 +181,17 @@ export const CostOverviewChart = ({ stroke="none" yAxisId={data.dailyCost.dataKey} /> - + {!hideTrendLine && ( + + )} {metric && ( = { + getLastCompleteBillingDate: jest.fn().mockResolvedValue('2022-10-30'), + getUserGroups: jest.fn().mockResolvedValue(['team-a']), + getGroupProjects: jest.fn().mockResolvedValue(['project-a', 'project-b']), + getCatalogEntityDailyCost: jest.fn().mockResolvedValue(mockGroupDailyCost), + getGroupDailyCost: jest.fn().mockResolvedValue({}), + getProjectDailyCost: jest.fn().mockResolvedValue({}), + getDailyMetricData: jest.fn().mockResolvedValue({}), + getProductInsights: jest.fn().mockResolvedValue({}), + getAlerts: jest.fn().mockResolvedValue({}), + }; + + return renderInTestApp( + + + + + + + + + + {children} + + + + + + + + + , + ); +} + +describe('', () => { + beforeEach(() => { + // @ts-expect-error: Since we have strictNullChecks enabled, this will throw an error as window.ResizeObserver + // it's not an optional operand + delete window.ResizeObserver; + window.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + })); + }); + + afterEach(() => { + window.ResizeObserver = ResizeObserver; + jest.restoreAllMocks(); + }); + + it('Renders without exploding', async () => { + const { getByText } = await renderInContext(); + expect(getByText('Cloud Cost')).toBeInTheDocument(); + }); +}); diff --git a/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx b/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx new file mode 100644 index 0000000000..9dda1c65e3 --- /dev/null +++ b/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx @@ -0,0 +1,161 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import { CostOverviewCard } from '../CostOverviewCard'; +import { + BillingDateProvider, + ConfigProvider, + CurrencyProvider, + FilterProvider, + GroupsProvider, + LoadingProvider, + ScrollProvider, + useFilters, + useLastCompleteBillingDate, + useLoading, +} from '../../hooks'; +import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider'; +import { Progress, WarningPanel } from '@backstage/core-components'; +import { default as MaterialAlert } from '@material-ui/lab/Alert'; +import { mapLoadingToProps } from '../CostInsightsPage/selector'; +import { intervalsOf } from '../../utils/duration'; +import { costInsightsApiRef } from '../../api'; +import { useApi } from '@backstage/core-plugin-api'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { Cost, Maybe } from '@backstage/plugin-cost-insights-common'; +import { stringifyEntityRef } from '@backstage/catalog-model'; + +export const EntityCostsCard = () => { + const client = useApi(costInsightsApiRef); + const { entity } = useEntity(); + + const { + loadingActions, + loadingGroups, + loadingBillingDate, + loadingInitial, + dispatchInitial, + dispatchInsights, + dispatchNone, + } = useLoading(mapLoadingToProps); + + /* eslint-disable react-hooks/exhaustive-deps */ + // The dispatchLoading functions are derived from loading state using mapLoadingToProps, to + // provide nicer props for the component. These are re-derived whenever loading state changes, + // which causes an infinite loop as product panels load and re-trigger the useEffect below. + // Since the functions don't change, we can memoize - but we trigger the same loop if we satisfy + // exhaustive-deps by including the function itself in dependencies. + + const dispatchLoadingInitial = useCallback(dispatchInitial, []); + const dispatchLoadingInsights = useCallback(dispatchInsights, []); + const dispatchLoadingNone = useCallback(dispatchNone, []); + /* eslint-enable react-hooks/exhaustive-deps */ + + const lastCompleteBillingDate = useLastCompleteBillingDate(); + const [dailyCost, setDailyCost] = useState>(null); + const [error, setError] = useState>(null); + const { pageFilters } = useFilters(p => p); + + useEffect(() => { + async function getInsights() { + setError(null); + try { + dispatchLoadingInsights(true); + const intervals = intervalsOf( + pageFilters.duration, + lastCompleteBillingDate, + ); + + const fetchedDailyCost = await client.getCatalogEntityDailyCost!( + stringifyEntityRef(entity), + intervals, + ); + setDailyCost(fetchedDailyCost); + } catch (e) { + setError(e); + dispatchLoadingNone(loadingActions); + } finally { + dispatchLoadingNone(loadingActions); + dispatchLoadingInitial(false); + dispatchLoadingInsights(false); + } + } + + // Wait for metadata to finish loading + if (!loadingBillingDate) { + getInsights(); + } + }, [ + client, + entity, + pageFilters, + loadingActions, + loadingGroups, + loadingBillingDate, + dispatchLoadingInsights, + dispatchLoadingInitial, + dispatchLoadingNone, + lastCompleteBillingDate, + ]); + + if (loadingInitial) { + return ; + } + + if (error) { + return {error.message}; + } + + if (!dailyCost) { + return No daily costs; + } + + return ; +}; + +export const EntityCosts = () => { + const client = useApi(costInsightsApiRef); + + if (!client.getCatalogEntityDailyCost) { + return ( + + ); + } + + return ( + + + + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/cost-insights/src/components/EntityCosts/index.ts b/plugins/cost-insights/src/components/EntityCosts/index.ts new file mode 100644 index 0000000000..b8d5106090 --- /dev/null +++ b/plugins/cost-insights/src/components/EntityCosts/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityCosts } from './EntityCosts'; diff --git a/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx b/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx index 090444f025..fd534eb8f0 100644 --- a/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx +++ b/plugins/cost-insights/src/components/MetricSelect/MetricSelect.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { InputLabel, FormControl, Select, MenuItem } from '@material-ui/core'; -import { Maybe, Metric } from '../../types'; +import { Maybe, Metric } from '@backstage/plugin-cost-insights-common'; import { useSelectStyles as useStyles } from '../../utils/styles'; export type MetricSelectProps = { diff --git a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx index dfac13dd42..a25e9274d3 100644 --- a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx +++ b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx @@ -15,13 +15,14 @@ */ import React from 'react'; -import { getByRole, waitFor } from '@testing-library/react'; +import { getByRole, screen, waitFor } from '@testing-library/react'; import { renderInTestApp } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { PeriodSelect, getDefaultOptions } from './PeriodSelect'; import { getDefaultPageFilters } from '../../utils/filters'; import { MockBillingDateProvider } from '../../testUtils'; -import { Group, Duration } from '../../types'; +import { Duration } from '../../types'; +import { Group } from '@backstage/plugin-cost-insights-common'; const DefaultPageFilters = getDefaultPageFilters([{ id: 'tools' }] as Group[]); const lastCompleteBillingDate = '2020-05-01'; @@ -29,7 +30,7 @@ const options = getDefaultOptions(lastCompleteBillingDate); describe('', () => { it('Renders without exploding', async () => { - const rendered = await renderInTestApp( + await renderInTestApp( @@ -39,11 +40,11 @@ describe('', () => { /> , ); - expect(rendered.getByTestId('period-select')).toBeInTheDocument(); + expect(screen.getByTestId('period-select')).toBeInTheDocument(); }); it('Should display all costGrowth period options', async () => { - const rendered = await renderInTestApp( + await renderInTestApp( @@ -53,13 +54,13 @@ describe('', () => { /> , ); - const periodSelectContainer = rendered.getByTestId('period-select'); + const periodSelectContainer = screen.getByTestId('period-select'); const button = getByRole(periodSelectContainer, 'button'); await userEvent.click(button); - await waitFor(() => rendered.getByText('Past 60 Days')); + await waitFor(() => screen.getByText('Past 60 Days')); options.forEach(option => expect( - rendered.getByTestId(`period-select-option-${option.value}`), + screen.getByTestId(`period-select-option-${option.value}`), ).toBeInTheDocument(), ); }); @@ -78,19 +79,19 @@ describe('', () => { ? Duration.P30D : DefaultPageFilters.duration; - const rendered = await renderInTestApp( + await renderInTestApp( , , ); - const periodSelect = rendered.getByTestId('period-select'); + const periodSelect = screen.getByTestId('period-select'); const button = getByRole(periodSelect, 'button'); await userEvent.click(button); await userEvent.click( - rendered.getByTestId(`period-select-option-${duration}`), + screen.getByTestId(`period-select-option-${duration}`), ); expect(mockOnSelect).toHaveBeenLastCalledWith(duration); }); diff --git a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx index 128c9d50b9..3c6b731ce8 100644 --- a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.test.tsx @@ -27,7 +27,7 @@ import { MockScrollProvider, MockLoadingProvider, } from '../../testUtils'; -import { Entity, Product } from '../../types'; +import { Entity, Product } from '@backstage/plugin-cost-insights-common'; // suppress recharts componentDidUpdate warnings jest.spyOn(console, 'warn').mockImplementation(() => {}); diff --git a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx index 3eba868843..9df763a5d4 100644 --- a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx +++ b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx @@ -19,7 +19,8 @@ import { Box, Typography } from '@material-ui/core'; import { default as Alert } from '@material-ui/lab/Alert'; import { costInsightsApiRef } from '../../api'; import { ProductInsightsCardList } from '../ProductInsightsCard/ProductInsightsCardList'; -import { Duration, Entity, Maybe, Product } from '../../types'; +import { Duration } from '../../types'; +import { Entity, Maybe, Product } from '@backstage/plugin-cost-insights-common'; import { intervalsOf, DEFAULT_DURATION } from '../../utils/duration'; import { DefaultLoadingAction, diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx index d3d12abe74..f804031d6f 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.test.tsx @@ -18,7 +18,8 @@ import React from 'react'; import { wrapInTestApp } from '@backstage/test-utils'; import { ProductEntityDialog } from './ProductEntityDialog'; import { render } from '@testing-library/react'; -import { Entity } from '../../types'; +import { Entity } from '@backstage/plugin-cost-insights-common'; +import { MockConfigProvider } from '../../testUtils'; const atomicEntity: Entity = { id: null, @@ -86,11 +87,13 @@ describe('', () => { it('Should show a tab for a single sub-entity type', () => { const { getByText } = render( wrapInTestApp( - , + + + , ), ); expect(getByText('Breakdown by SKU')).toBeInTheDocument(); @@ -99,11 +102,13 @@ describe('', () => { it('Should show tabs when multiple sub-entity types exist', () => { const { getByText } = render( wrapInTestApp( - , + + + , ), ); expect(getByText('Breakdown by SKU')).toBeInTheDocument(); diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx index 50b2ff993c..55fa795728 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityDialog.tsx @@ -18,7 +18,7 @@ import React, { useState } from 'react'; import { Dialog, IconButton } from '@material-ui/core'; import { default as CloseButton } from '@material-ui/icons/Close'; import { useEntityDialogStyles as useStyles } from '../../utils/styles'; -import { Entity } from '../../types'; +import { Entity } from '@backstage/plugin-cost-insights-common'; import { ProductEntityTable, ProductEntityTableOptions, diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx index 95ba4f2f2d..affb16cb30 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductEntityTable.tsx @@ -17,11 +17,16 @@ import React from 'react'; import classnames from 'classnames'; import { Typography } from '@material-ui/core'; -import { costFormatter, formatChange } from '../../utils/formatters'; +import { formatChange } from '../../utils/formatters'; import { useEntityDialogStyles as useStyles } from '../../utils/styles'; import { CostGrowthIndicator } from '../CostGrowth'; -import { BarChartOptions, ChangeStatistic, Entity } from '../../types'; +import { BarChartOptions } from '../../types'; +import { + ChangeStatistic, + Entity, +} from '@backstage/plugin-cost-insights-common'; import { Table, TableColumn } from '@backstage/core-components'; +import { useConfig } from '../../hooks'; export type ProductEntityTableOptions = Partial< Pick @@ -35,36 +40,38 @@ type RowData = { change: ChangeStatistic; }; -function createRenderer(col: keyof RowData, classes: Record) { - return function render(rowData: {}): JSX.Element { - const row = rowData as RowData; - const rowStyles = classnames(classes.row, { - [classes.rowTotal]: row.id === 'total', - [classes.colFirst]: col === 'label', - [classes.colLast]: col === 'change', - }); +const createRenderer = + (baseCurrency: Intl.NumberFormat) => + (col: keyof RowData, classes: Record) => { + return function render(rowData: {}): JSX.Element { + const row = rowData as RowData; + const rowStyles = classnames(classes.row, { + [classes.rowTotal]: row.id === 'total', + [classes.colFirst]: col === 'label', + [classes.colLast]: col === 'change', + }); - switch (col) { - case 'previous': - case 'current': - return ( - - {costFormatter.format(row[col])} - - ); - case 'change': - return ( - - ); - default: - return {row.label}; - } + switch (col) { + case 'previous': + case 'current': + return ( + + {baseCurrency.format(row[col])} + + ); + case 'change': + return ( + + ); + default: + return {row.label}; + } + }; }; -} // material-table does not support fixed rows. Override the sorting algorithm // to force Total row to bottom by default or when a user sort toggles a column. @@ -99,6 +106,7 @@ export const ProductEntityTable = ({ options, }: ProductEntityTableProps) => { const classes = useStyles(); + const { baseCurrency } = useConfig(); const entities = entity.entities[entityLabel]; const data = Object.assign( @@ -116,7 +124,7 @@ export const ProductEntityTable = ({ { field: 'label', title: {entityLabel}, - render: createRenderer('label', classes), + render: createRenderer(baseCurrency)('label', classes), customSort: createSorter('label'), width: '33.33%', }, @@ -126,7 +134,7 @@ export const ProductEntityTable = ({ {data.previousName} ), align: 'right', - render: createRenderer('previous', classes), + render: createRenderer(baseCurrency)('previous', classes), customSort: createSorter('previous'), }, { @@ -135,14 +143,14 @@ export const ProductEntityTable = ({ {data.currentName} ), align: 'right', - render: createRenderer('current', classes), + render: createRenderer(baseCurrency)('current', classes), customSort: createSorter('current'), }, { field: 'change', title: Change, align: 'right', - render: createRenderer('change', classes), + render: createRenderer(baseCurrency)('change', classes), customSort: createSorter('change'), }, ]; diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx index 737019e77a..b24b3bd366 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx @@ -28,7 +28,8 @@ import { MockScrollProvider, MockLoadingProvider, } from '../../testUtils'; -import { Duration, Entity, Product } from '../../types'; +import { Duration } from '../../types'; +import { Entity, Product } from '@backstage/plugin-cost-insights-common'; // suppress recharts componentDidUpdate warnings jest.spyOn(console, 'warn').mockImplementation(() => {}); diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx index 2b4ee4b0db..a587bfb26e 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.tsx @@ -28,7 +28,8 @@ import { PeriodSelect } from '../PeriodSelect'; import { ProductInsightsChart } from './ProductInsightsChart'; import { useProductInsightsCardStyles as useStyles } from '../../utils/styles'; import { DefaultLoadingAction } from '../../utils/loading'; -import { Duration, Entity, Maybe, Product } from '../../types'; +import { Duration } from '../../types'; +import { Entity, Maybe, Product } from '@backstage/plugin-cost-insights-common'; import { MapLoadingToProps, useLastCompleteBillingDate, diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCardList.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCardList.tsx index ecf60b4bef..3ae97daecb 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCardList.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCardList.tsx @@ -17,7 +17,8 @@ import React from 'react'; import { Box, CircularProgress, Collapse } from '@material-ui/core'; import { ProductInsightsCard } from './ProductInsightsCard'; -import { Duration, Entity, Product } from '../../types'; +import { Duration } from '../../types'; +import { Entity, Product } from '@backstage/plugin-cost-insights-common'; import { ProductState } from '../../utils/loading'; type ProductInsightsCardListProps = { diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx index b77b415e1e..2976b1f30f 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsChart.tsx @@ -48,9 +48,11 @@ import { useProductInsightsChartStyles as useStyles, useBarChartLayoutStyles as useLayoutStyles, } from '../../utils/styles'; -import { Duration, Entity, Maybe } from '../../types'; +import { Duration } from '../../types'; +import { Entity, Maybe } from '@backstage/plugin-cost-insights-common'; import { choose } from '../../utils/change'; -import { TooltipRenderer } from '../../types/Tooltip'; +import { TooltipRenderer } from '../../types'; +import { useConfig } from '../../hooks'; export type ProductInsightsChartProps = { billingDate: string; @@ -65,6 +67,7 @@ export const ProductInsightsChart = ({ }: ProductInsightsChartProps) => { const classes = useStyles(); const layoutClasses = useLayoutStyles(); + const { baseCurrency } = useConfig(); // Only a single entities Record for the root product entity is supported const entities = useMemo(() => { @@ -131,7 +134,7 @@ export const ProductInsightsChart = ({ const id = label === '' ? null : label; const title = titleOf(label); - const items = payload.map(tooltipItemOf).filter(notEmpty); + const items = payload.map(tooltipItemOf(baseCurrency)).filter(notEmpty); const activeEntity = findAlways(entities, e => e.id === id); const breakdowns = Object.keys(activeEntity.entities); diff --git a/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx b/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx index 35b00ce6f6..0435210a98 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthInstructionsPage/ProjectGrowthInstructionsPage.tsx @@ -23,10 +23,9 @@ import { Alert, DEFAULT_DATE_FORMAT, Duration, - Entity, - Product, ProjectGrowthData, } from '../../types'; +import { Entity, Product } from '@backstage/plugin-cost-insights-common'; import { ProjectGrowthAlert } from '../../alerts'; import { InfoCard } from '@backstage/core-components'; diff --git a/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx b/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx index 798b29de0d..7dd5b4253f 100644 --- a/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx +++ b/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { getByRole, waitFor } from '@testing-library/react'; +import { getByRole, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ProjectSelect } from './ProjectSelect'; import { MockFilterProvider } from '../../testUtils'; @@ -42,23 +42,19 @@ describe('', () => { }); it('Renders without exploding', async () => { - const rendered = await renderInTestApp(Component); - expect(rendered.getByText('All Projects')).toBeInTheDocument(); + await renderInTestApp(Component); + expect(screen.getByText('All Projects')).toBeInTheDocument(); }); it('shows all projects in the filter select', async () => { - const rendered = await renderInTestApp(Component); - const projectSelectContainer = rendered.getByTestId( - 'project-filter-select', - ); + await renderInTestApp(Component); + const projectSelectContainer = screen.getByTestId('project-filter-select'); const button = getByRole(projectSelectContainer, 'button'); await userEvent.click(button); - await waitFor(() => rendered.getByTestId('option-all')); + await waitFor(() => screen.getByTestId('option-all')); mockProjects.forEach(project => - expect( - rendered.getByText(project.name ?? project.id), - ).toBeInTheDocument(), + expect(screen.getByText(project.name ?? project.id)).toBeInTheDocument(), ); }); }); diff --git a/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.tsx b/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.tsx index d46123b001..c83fa67d94 100644 --- a/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.tsx +++ b/plugins/cost-insights/src/components/ProjectSelect/ProjectSelect.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; import { MenuItem, Select } from '@material-ui/core'; -import { Maybe, Project } from '../../types'; +import { Maybe, Project } from '@backstage/plugin-cost-insights-common'; import { useSelectStyles as useStyles } from '../../utils/styles'; type ProjectSelectProps = { diff --git a/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx b/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx index 06353535a5..3ef6e54a53 100644 --- a/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx +++ b/plugins/cost-insights/src/components/UnlabeledDataflowAlertCard/UnlabeledDataflowAlertCard.test.tsx @@ -19,6 +19,7 @@ import { UnlabeledDataflowAlertCard } from './UnlabeledDataflowAlertCard'; import { createMockUnlabeledDataflowData, createMockUnlabeledDataflowAlertProject, + MockConfigProvider, } from '../../testUtils'; import { renderInTestApp } from '@backstage/test-utils'; @@ -61,9 +62,11 @@ describe('', () => { 'projects with unlabeled Dataflow jobs in the last 30 days.', ); const rendered = await renderInTestApp( - , + + + , ); expect(rendered.getByText(subheader)).toBeInTheDocument(); }); @@ -71,9 +74,11 @@ describe('', () => { it('renders the correct subheader for a single project', async () => { const subheader = new RegExp('1 project'); const rendered = await renderInTestApp( - , + + + , ); expect(rendered.getByText(subheader)).toBeInTheDocument(); }); diff --git a/plugins/cost-insights/src/example/alerts/KubernetesMigrationAlert.tsx b/plugins/cost-insights/src/example/alerts/KubernetesMigrationAlert.tsx index e9d5877bf0..eb5423148d 100644 --- a/plugins/cost-insights/src/example/alerts/KubernetesMigrationAlert.tsx +++ b/plugins/cost-insights/src/example/alerts/KubernetesMigrationAlert.tsx @@ -24,9 +24,11 @@ import { AlertOptions, AlertStatus, AlertSnoozeFormData, +} from '../../types'; +import { ChangeStatistic, Entity, -} from '../../types'; +} from '@backstage/plugin-cost-insights-common'; import { KubernetesMigrationDismissForm, KubernetesMigrationDismissFormData, diff --git a/plugins/cost-insights/src/example/client.ts b/plugins/cost-insights/src/example/client.ts index 9c3e20c2b3..160c0a691b 100644 --- a/plugins/cost-insights/src/example/client.ts +++ b/plugins/cost-insights/src/example/client.ts @@ -19,15 +19,17 @@ import { DateTime } from 'luxon'; import { CostInsightsApi, ProductInsightsOptions } from '../api'; import { Alert, - Cost, DEFAULT_DATE_FORMAT, + ProjectGrowthData, + UnlabeledDataflowData, +} from '../types'; +import { Entity, Group, MetricData, Project, - ProjectGrowthData, - UnlabeledDataflowData, -} from '../types'; + Cost, +} from '@backstage/plugin-cost-insights-common'; import { KubernetesMigrationAlert } from './alerts'; import { ProjectGrowthAlert, UnlabeledDataflowAlert } from '../alerts'; import { @@ -92,6 +94,29 @@ export class ExampleCostInsightsClient implements CostInsightsApi { return cost; } + async getCatalogEntityDailyCost( + entityRef: string, + intervals: string, + ): Promise { + const aggregation = aggregationFor(intervals, 8_000); + const groupDailyCost: Cost = await this.request( + { entityRef, intervals }, + { + aggregation: aggregation, + change: changeOf(aggregation), + trendline: trendlineOf(aggregation), + // Optional field providing cost groupings / breakdowns keyed by the type. In this example, + // daily cost grouped by cloud product OR by project / billing account. + groupedCosts: { + product: getGroupedProducts(intervals), + project: getGroupedProjects(intervals), + }, + }, + ); + + return groupDailyCost; + } + async getGroupDailyCost(group: string, intervals: string): Promise { const aggregation = aggregationFor(intervals, 8_000); const groupDailyCost: Cost = await this.request( diff --git a/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChart.tsx b/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChart.tsx index 68a92aa877..bdc7af551a 100644 --- a/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChart.tsx +++ b/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChart.tsx @@ -19,9 +19,9 @@ import { BarChart } from '../../../components'; import { BarChartOptions, CostInsightsTheme, - Entity, ResourceData, } from '../../../types'; +import { Entity } from '@backstage/plugin-cost-insights-common'; import { useTheme } from '@material-ui/core'; type MigrationBarChartProps = { diff --git a/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.tsx b/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.tsx index 5b62b4b7bc..1b46b15a10 100644 --- a/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.tsx +++ b/plugins/cost-insights/src/example/components/KubernetesMigrationAlertCard/KubernetesMigrationBarChartLegend.tsx @@ -17,7 +17,8 @@ import React from 'react'; import { Box, useTheme } from '@material-ui/core'; import { CostGrowth, LegendItem } from '../../../components'; -import { ChangeStatistic, CostInsightsTheme, Duration } from '../../../types'; +import { CostInsightsTheme, Duration } from '../../../types'; +import { ChangeStatistic } from '@backstage/plugin-cost-insights-common'; import { monthOf } from '../../../utils/formatters'; export type MigrationBarChartLegendProps = { diff --git a/plugins/cost-insights/src/example/forms/KubernetesMigrationDismissForm.tsx b/plugins/cost-insights/src/example/forms/KubernetesMigrationDismissForm.tsx index 285cc15f94..7efd32616c 100644 --- a/plugins/cost-insights/src/example/forms/KubernetesMigrationDismissForm.tsx +++ b/plugins/cost-insights/src/example/forms/KubernetesMigrationDismissForm.tsx @@ -28,7 +28,8 @@ import { FormGroup, Typography, } from '@material-ui/core'; -import { AlertFormProps, Entity } from '../../types'; +import { AlertFormProps } from '../../types'; +import { Entity } from '@backstage/plugin-cost-insights-common'; import { KubernetesMigrationAlert } from '../alerts'; import { findAlways } from '../../utils/assert'; diff --git a/plugins/cost-insights/src/example/templates/CostInsightsClient.ts b/plugins/cost-insights/src/example/templates/CostInsightsClient.ts index fc68eb62c7..50c3bb1470 100644 --- a/plugins/cost-insights/src/example/templates/CostInsightsClient.ts +++ b/plugins/cost-insights/src/example/templates/CostInsightsClient.ts @@ -87,6 +87,17 @@ export class CostInsightsClient implements CostInsightsApi { } } + async getCatalogEntityDailyCost(catalogEntityRef: string, intervals: string): Promise { + return { + id: 'remove-me', + aggregation: [], + change: { + ratio: 0, + amount: 0 + } + } + } + async getProductInsights(options: ProductInsightsOptions): Promise { return { id: 'remove-me', diff --git a/plugins/cost-insights/src/forms/AlertDismissForm.tsx b/plugins/cost-insights/src/forms/AlertDismissForm.tsx index 9697672789..7506fe8ee1 100644 --- a/plugins/cost-insights/src/forms/AlertDismissForm.tsx +++ b/plugins/cost-insights/src/forms/AlertDismissForm.tsx @@ -37,8 +37,8 @@ import { AlertDismissReason, AlertDismissOptions, AlertDismissFormData, - Maybe, } from '../types'; +import { Maybe } from '@backstage/plugin-cost-insights-common'; import { useAlertDialogStyles as useStyles } from '../utils/styles'; export type AlertDismissFormProps = AlertFormProps; diff --git a/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx b/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx index 0dcc0a949e..3f87c87771 100644 --- a/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx +++ b/plugins/cost-insights/src/forms/AlertSnoozeForm.tsx @@ -35,10 +35,10 @@ import { AlertFormProps, Duration, DEFAULT_DATE_FORMAT, - Maybe, AlertSnoozeFormData, AlertSnoozeOptions, } from '../types'; +import { Maybe } from '@backstage/plugin-cost-insights-common'; import { useAlertDialogStyles as useStyles } from '../utils/styles'; import { intervalsOf } from '../utils/duration'; diff --git a/plugins/cost-insights/src/hooks/useConfig.tsx b/plugins/cost-insights/src/hooks/useConfig.tsx index 322e1e35f1..f42cea38c3 100644 --- a/plugins/cost-insights/src/hooks/useConfig.tsx +++ b/plugins/cost-insights/src/hooks/useConfig.tsx @@ -22,10 +22,11 @@ import React, { useState, } from 'react'; import { Config as BackstageConfig } from '@backstage/config'; -import { Currency, Icon, Metric, Product } from '../types'; +import { Currency, EngineerThreshold, Icon } from '../types'; +import { Metric, Product } from '@backstage/plugin-cost-insights-common'; import { getIcon } from '../utils/navigation'; import { validateCurrencies, validateMetrics } from '../utils/config'; -import { defaultCurrencies } from '../utils/currency'; +import { createCurrencyFormat, defaultCurrencies } from '../utils/currency'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; /* @@ -46,6 +47,11 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; * default: true * metricB: * name: Metric B + * baseCurrency: + * locale: nl-NL + * options: + * currency: EUR + * minimumFractionDigits: 3 * currencies: * currencyA: * label: Currency A @@ -60,10 +66,12 @@ import { configApiRef, useApi } from '@backstage/core-plugin-api'; /** @public */ export type ConfigContextProps = { + baseCurrency: Intl.NumberFormat; metrics: Metric[]; products: Product[]; icons: Icon[]; engineerCost: number; + engineerThreshold: number; currencies: Currency[]; }; @@ -72,10 +80,12 @@ export const ConfigContext = createContext( ); const defaultState: ConfigContextProps = { + baseCurrency: createCurrencyFormat(), metrics: [], products: [], icons: [], engineerCost: 0, + engineerThreshold: EngineerThreshold, currencies: defaultCurrencies, }; @@ -110,6 +120,42 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => { return []; } + function getBaseCurrency(): Intl.NumberFormat { + const baseCurrency = c.getOptionalConfig('costInsights.baseCurrency'); + if (baseCurrency) { + const options = baseCurrency.getOptionalConfig('options'); + return new Intl.NumberFormat( + baseCurrency.getOptionalString('locale'), + options + ? { + localeMatcher: options.getOptionalString('localeMatcher'), + style: 'currency', + currency: options.getOptionalString('currency'), + currencySign: options.getOptionalString('currencySign'), + useGrouping: options.getOptionalBoolean('useGrouping'), + minimumIntegerDigits: options.getOptionalNumber( + 'minimumIntegerDigits', + ), + minimumFractionDigits: options.getOptionalNumber( + 'minimumFractionDigits', + ), + maximumFractionDigits: options.getOptionalNumber( + 'maximumFractionDigits', + ), + minimumSignificantDigits: options.getOptionalNumber( + 'minimumSignificantDigits', + ), + maximumSignificantDigits: options.getOptionalNumber( + 'maximumSignificantDigits', + ), + } + : undefined, + ); + } + + return defaultState.baseCurrency; + } + function getCurrencies(): Currency[] { const currencies = c.getOptionalConfig('costInsights.currencies'); if (currencies) { @@ -140,10 +186,19 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => { return c.getNumber('costInsights.engineerCost'); } + function getEngineerThreshold(): number { + return ( + c.getOptionalNumber('costInsights.engineerThreshold') ?? + defaultState.engineerThreshold + ); + } + function getConfig() { + const baseCurrency = getBaseCurrency(); const products = getProducts(); const metrics = getMetrics(); const engineerCost = getEngineerCost(); + const engineerThreshold = getEngineerThreshold(); const icons = getIcons(); const currencies = getCurrencies(); @@ -152,9 +207,11 @@ export const ConfigProvider = ({ children }: PropsWithChildren<{}>) => { setConfig(prevState => ({ ...prevState, + baseCurrency, metrics, products, engineerCost, + engineerThreshold, icons, currencies, })); diff --git a/plugins/cost-insights/src/hooks/useFilters.tsx b/plugins/cost-insights/src/hooks/useFilters.tsx index abe7a132cc..e888da369e 100644 --- a/plugins/cost-insights/src/hooks/useFilters.tsx +++ b/plugins/cost-insights/src/hooks/useFilters.tsx @@ -23,7 +23,8 @@ import React, { useState, } from 'react'; import { Alert } from '@material-ui/lab'; -import { Maybe, PageFilters, ProductFilters } from '../types'; +import { PageFilters, ProductFilters } from '../types'; +import { Maybe } from '@backstage/plugin-cost-insights-common'; import { useLocation, useNavigate } from 'react-router-dom'; import { stringify, diff --git a/plugins/cost-insights/src/hooks/useGroups.tsx b/plugins/cost-insights/src/hooks/useGroups.tsx index de0e253763..ec10faa7de 100644 --- a/plugins/cost-insights/src/hooks/useGroups.tsx +++ b/plugins/cost-insights/src/hooks/useGroups.tsx @@ -23,7 +23,7 @@ import React, { import { Alert } from '@material-ui/lab'; import { costInsightsApiRef } from '../api'; import { MapLoadingToProps, useLoading } from './useLoading'; -import { Group, Maybe } from '../types'; +import { Group, Maybe } from '@backstage/plugin-cost-insights-common'; import { DefaultLoadingAction } from '../utils/loading'; import { useApi, identityApiRef } from '@backstage/core-plugin-api'; import { DEFAULT_NAMESPACE, parseEntityRef } from '@backstage/catalog-model'; diff --git a/plugins/cost-insights/src/hooks/useLastCompleteBillingDate.tsx b/plugins/cost-insights/src/hooks/useLastCompleteBillingDate.tsx index d8236ed61a..24c5716442 100644 --- a/plugins/cost-insights/src/hooks/useLastCompleteBillingDate.tsx +++ b/plugins/cost-insights/src/hooks/useLastCompleteBillingDate.tsx @@ -25,7 +25,7 @@ import { Alert } from '@material-ui/lab'; import { costInsightsApiRef } from '../api'; import { MapLoadingToProps, useLoading } from './useLoading'; import { DefaultLoadingAction } from '../utils/loading'; -import { Maybe } from '../types'; +import { Maybe } from '@backstage/plugin-cost-insights-common'; import { useApi } from '@backstage/core-plugin-api'; type BillingDateProviderLoadingProps = { diff --git a/plugins/cost-insights/src/hooks/useScroll.tsx b/plugins/cost-insights/src/hooks/useScroll.tsx index 5ddc60b8ff..3e2bbc6c24 100644 --- a/plugins/cost-insights/src/hooks/useScroll.tsx +++ b/plugins/cost-insights/src/hooks/useScroll.tsx @@ -20,7 +20,7 @@ import React, { useContext, PropsWithChildren, } from 'react'; -import { Maybe } from '../types'; +import { Maybe } from '@backstage/plugin-cost-insights-common'; export type ScrollTo = Maybe; diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index 2c46156005..8a17b1bf43 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -24,6 +24,7 @@ export { costInsightsPlugin, costInsightsPlugin as plugin, CostInsightsPage, + EntityCostInsightsContent, CostInsightsProjectGrowthInstructionsPage, CostInsightsLabelDataflowInstructionsPage, } from './plugin'; diff --git a/plugins/cost-insights/src/options.ts b/plugins/cost-insights/src/options.ts new file mode 100644 index 0000000000..287935d703 --- /dev/null +++ b/plugins/cost-insights/src/options.ts @@ -0,0 +1,29 @@ +/* + * 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 { usePluginOptions } from '@backstage/core-plugin-api'; + +export type CostInsightsPluginOptions = { + hideTrendLine?: boolean; +}; + +/** @ignore */ +export type CostInsightsInputPluginOptions = { + hideTrendLine?: boolean; +}; + +export const useCostInsightsOptions = () => + usePluginOptions(); diff --git a/plugins/cost-insights/src/plugin.ts b/plugins/cost-insights/src/plugin.ts index 789d803438..c863c9e03a 100644 --- a/plugins/cost-insights/src/plugin.ts +++ b/plugins/cost-insights/src/plugin.ts @@ -19,6 +19,10 @@ import { createRouteRef, createRoutableExtension, } from '@backstage/core-plugin-api'; +import { + CostInsightsInputPluginOptions, + CostInsightsPluginOptions, +} from './options'; export const rootRouteRef = createRouteRef({ id: 'cost-insights', @@ -41,6 +45,14 @@ export const costInsightsPlugin = createPlugin({ growthAlerts: projectGrowthAlertRef, unlabeledDataflowAlerts: unlabeledDataflowAlertRef, }, + __experimentalConfigure( + options?: CostInsightsInputPluginOptions, + ): CostInsightsPluginOptions { + const defaultOptions = { + hideTrendLine: false, + }; + return { ...defaultOptions, ...options }; + }, }); /** @public */ @@ -53,6 +65,20 @@ export const CostInsightsPage = costInsightsPlugin.provide( }), ); +/** + * An extension for displaying costs on an entity page. + * + * @public + */ +export const EntityCostInsightsContent = costInsightsPlugin.provide( + createRoutableExtension({ + name: 'EntityCostInsightsContent', + component: () => + import('./components/EntityCosts').then(m => m.EntityCosts), + mountPoint: rootRouteRef, + }), +); + /** @public */ export const CostInsightsProjectGrowthInstructionsPage = costInsightsPlugin.provide( diff --git a/plugins/cost-insights/src/testUtils/mockData.ts b/plugins/cost-insights/src/testUtils/mockData.ts index 45a514ff8f..67cb4d9035 100644 --- a/plugins/cost-insights/src/testUtils/mockData.ts +++ b/plugins/cost-insights/src/testUtils/mockData.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { DateAggregation, Entity } from '../types'; +import { + DateAggregation, + Entity, +} from '@backstage/plugin-cost-insights-common'; export const MockAggregatedDailyCosts: DateAggregation[] = [ { diff --git a/plugins/cost-insights/src/testUtils/products.ts b/plugins/cost-insights/src/testUtils/products.ts index 1daab969de..200c1014b0 100644 --- a/plugins/cost-insights/src/testUtils/products.ts +++ b/plugins/cost-insights/src/testUtils/products.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, Product } from '../types'; +import { Entity, Product } from '@backstage/plugin-cost-insights-common'; import { findAlways } from '../utils/assert'; type mockEntityRenderer = (entity: T) => T; diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index 8088fca3c6..9522dfb702 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -15,19 +15,24 @@ */ import React, { PropsWithChildren } from 'react'; -import { LoadingContext, LoadingContextProps } from '../hooks/useLoading'; -import { GroupsContext, GroupsContextProps } from '../hooks/useGroups'; -import { FilterContext, FilterContextProps } from '../hooks/useFilters'; -import { ConfigContext, ConfigContextProps } from '../hooks/useConfig'; -import { CurrencyContext, CurrencyContextProps } from '../hooks/useCurrency'; import { + LoadingContext, + LoadingContextProps, + GroupsContext, + GroupsContextProps, + FilterContext, + FilterContextProps, + ConfigContext, + ConfigContextProps, + CurrencyContext, + CurrencyContextProps, BillingDateContext, BillingDateContextProps, -} from '../hooks/useLastCompleteBillingDate'; -import { ScrollContext, ScrollContextProps } from '../hooks/useScroll'; -import { Group, Duration } from '../types'; - -export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }]; + ScrollContext, + ScrollContextProps, +} from '../hooks'; +import { Duration, EngineerThreshold } from '../types'; +import { createCurrencyFormat } from '../utils/currency'; export type MockFilterProviderProps = PropsWithChildren< Partial @@ -85,10 +90,12 @@ export const MockConfigProvider = (props: MockConfigProviderProps) => { const { children, ...context } = props; const defaultContext: ConfigContextProps = { + baseCurrency: createCurrencyFormat(), metrics: [], products: [], icons: [], engineerCost: 0, + engineerThreshold: EngineerThreshold, currencies: [], }; diff --git a/plugins/cost-insights/src/testUtils/testUtils.ts b/plugins/cost-insights/src/testUtils/testUtils.ts index e0ed1aa933..4e1bea347d 100644 --- a/plugins/cost-insights/src/testUtils/testUtils.ts +++ b/plugins/cost-insights/src/testUtils/testUtils.ts @@ -16,14 +16,13 @@ import { DateTime } from 'luxon'; import regression, { DataPoint } from 'regression'; +import { Duration, DEFAULT_DATE_FORMAT } from '../types'; import { ChangeStatistic, - Duration, Entity, Trendline, DateAggregation, - DEFAULT_DATE_FORMAT, -} from '../types'; +} from '@backstage/plugin-cost-insights-common'; import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; import { MockComputeEngineInsights, diff --git a/plugins/cost-insights/src/utils/alerts.tsx b/plugins/cost-insights/src/utils/alerts.tsx index 2039a5b617..9efc8a4f3b 100644 --- a/plugins/cost-insights/src/utils/alerts.tsx +++ b/plugins/cost-insights/src/utils/alerts.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Alert, AlertForm, AlertStatus, Maybe } from '../types'; +import { Alert, AlertForm, AlertStatus } from '../types'; +import { Maybe } from '@backstage/plugin-cost-insights-common'; import { AlertAcceptForm, AlertDismissForm, AlertSnoozeForm } from '../forms'; const createAlertHandler = (status?: AlertStatus) => (alert: Alert) => diff --git a/plugins/cost-insights/src/utils/change.test.ts b/plugins/cost-insights/src/utils/change.test.ts index b17e15797e..79f68db716 100644 --- a/plugins/cost-insights/src/utils/change.test.ts +++ b/plugins/cost-insights/src/utils/change.test.ts @@ -20,8 +20,8 @@ import { ChangeThreshold, EngineerThreshold, Duration, - Cost, } from '../types'; +import { Cost } from '@backstage/plugin-cost-insights-common'; import { MockAggregatedDailyCosts, trendlineOf, changeOf } from '../testUtils'; const GrowthMap = { @@ -63,7 +63,7 @@ describe.each` expected: GrowthType; }) => { it(`should display ${GrowthMap[expected]}`, () => { - expect(growthOf({ ratio, amount })).toBe(expected); + expect(growthOf({ ratio, amount }, EngineerThreshold)).toBe(expected); }); }, ); diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index 3a0d19b123..33a5eb4a9b 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -14,23 +14,23 @@ * limitations under the License. */ +import { ChangeThreshold, GrowthType, Duration } from '../types'; import { Cost, ChangeStatistic, - ChangeThreshold, - EngineerThreshold, - GrowthType, MetricData, - Duration, DateAggregation, -} from '../types'; +} from '@backstage/plugin-cost-insights-common'; import { DateTime, Duration as LuxonDuration } from 'luxon'; import { inclusiveStartDateOf } from './duration'; import { notEmpty } from './assert'; // Used for displaying status colors -export function growthOf(change: ChangeStatistic): GrowthType { - const exceedsEngineerThreshold = Math.abs(change.amount) >= EngineerThreshold; +export function growthOf( + change: ChangeStatistic, + engineerThreshold: number, +): GrowthType { + const exceedsEngineerThreshold = Math.abs(change.amount) >= engineerThreshold; if (notEmpty(change.ratio)) { if (exceedsEngineerThreshold && change.ratio >= ChangeThreshold.upper) { @@ -41,9 +41,12 @@ export function growthOf(change: ChangeStatistic): GrowthType { return GrowthType.Savings; } } else { - if (exceedsEngineerThreshold && change.amount > 0) return GrowthType.Excess; - if (exceedsEngineerThreshold && change.amount < 0) + if (exceedsEngineerThreshold && change.amount > 0) { + return GrowthType.Excess; + } + if (exceedsEngineerThreshold && change.amount < 0) { return GrowthType.Savings; + } } return GrowthType.Negligible; diff --git a/plugins/cost-insights/src/utils/charts.ts b/plugins/cost-insights/src/utils/charts.ts index 2a6d0b2ad6..e7bcd7bd33 100644 --- a/plugins/cost-insights/src/utils/charts.ts +++ b/plugins/cost-insights/src/utils/charts.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { DateAggregation, Trendline, ChartData } from '../types'; +import { + DateAggregation, + Trendline, +} from '@backstage/plugin-cost-insights-common'; +import { ChartData } from '../types'; export function trendFrom(trendline: Trendline, date: number): number { return trendline.slope * (date / 1000) + trendline.intercept; diff --git a/plugins/cost-insights/src/utils/config.ts b/plugins/cost-insights/src/utils/config.ts index 496021d70e..3a2e858de3 100644 --- a/plugins/cost-insights/src/utils/config.ts +++ b/plugins/cost-insights/src/utils/config.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Metric, Currency } from '../types'; +import { Currency } from '../types'; +import { Metric } from '@backstage/plugin-cost-insights-common'; export function validateMetrics(metrics: Metric[]) { const defaults = metrics.filter(metric => metric.default); diff --git a/plugins/cost-insights/src/utils/currency.ts b/plugins/cost-insights/src/utils/currency.ts index 60115aad3a..98c968fc53 100644 --- a/plugins/cost-insights/src/utils/currency.ts +++ b/plugins/cost-insights/src/utils/currency.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Currency, CurrencyType, Duration } from '../types'; -import { assertNever } from '../utils/assert'; +import { assertNever } from './assert'; export const rateOf = (cost: number, duration: Duration) => { switch (duration) { @@ -61,3 +61,12 @@ export const defaultCurrencies: Currency[] = [ rate: 5.5, }, ]; + +export const createCurrencyFormat = ( + currency: string = 'USD', + locale: string = 'en-US', +) => + new Intl.NumberFormat(locale, { + currency, + style: 'currency', + }); diff --git a/plugins/cost-insights/src/utils/filters.ts b/plugins/cost-insights/src/utils/filters.ts index f0252edd45..d44042d358 100644 --- a/plugins/cost-insights/src/utils/filters.ts +++ b/plugins/cost-insights/src/utils/filters.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Duration, Group, PageFilters } from '../types'; +import { Duration, PageFilters } from '../types'; +import { Group } from '@backstage/plugin-cost-insights-common'; export function getDefaultPageFilters(groups: Group[]): PageFilters { return { diff --git a/plugins/cost-insights/src/utils/formatters.test.ts b/plugins/cost-insights/src/utils/formatters.test.ts index 8bf0a6a9d7..1190ac17ae 100644 --- a/plugins/cost-insights/src/utils/formatters.test.ts +++ b/plugins/cost-insights/src/utils/formatters.test.ts @@ -21,6 +21,7 @@ import { quarterOf, } from './formatters'; import { Duration } from '../types'; +import { createCurrencyFormat } from './currency'; Date.now = jest.fn(() => new Date(Date.parse('2019-12-07')).valueOf()); @@ -39,7 +40,7 @@ describe('date formatters', () => { 0.00000040925, 0.21, 0.0000004, 0.4139877878, 0.00000234566, ]; const formattedValues = values.map(val => - lengthyCurrencyFormatter.format(val), + lengthyCurrencyFormatter(createCurrencyFormat()).format(val), ); expect(formattedValues).toEqual([ '$0.00000041', @@ -49,6 +50,22 @@ describe('date formatters', () => { '$0.0000023', ]); }); + + it('Correctly formats values in euros to two significant digits', () => { + const values = [ + 0.00000040925, 0.21, 0.0000004, 0.4139877878, 0.00000234566, + ]; + const formattedValues = values.map(val => + lengthyCurrencyFormatter(createCurrencyFormat('EUR')).format(val), + ); + expect(formattedValues).toEqual([ + '€0.00000041', + '€0.21', + '€0.00000040', + '€0.41', + '€0.0000023', + ]); + }); }); describe.each` diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index e9b19097a4..e715aebbd7 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -16,8 +16,9 @@ import { DateTime, Duration as LuxonDuration } from 'luxon'; import pluralize from 'pluralize'; -import { ChangeStatistic, Duration } from '../types'; -import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; +import { Duration } from '../types'; +import { ChangeStatistic } from '@backstage/plugin-cost-insights-common'; +import { inclusiveEndDateOf, inclusiveStartDateOf } from './duration'; import { notEmpty } from './assert'; export type Period = { @@ -25,25 +26,28 @@ export type Period = { periodEnd: string; }; -export const costFormatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', -}); +export const currencyFormatter = (currency: Intl.NumberFormat) => { + const options = currency.resolvedOptions(); -export const currencyFormatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 0, - maximumFractionDigits: 0, -}); + return new Intl.NumberFormat(options.locale, { + style: 'currency', + currency: options.currency, + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }); +}; -export const lengthyCurrencyFormatter = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 0, - minimumSignificantDigits: 2, - maximumSignificantDigits: 2, -}); +export const lengthyCurrencyFormatter = (currency: Intl.NumberFormat) => { + const options = currency.resolvedOptions(); + + return new Intl.NumberFormat(options.locale, { + style: 'currency', + currency: options.currency, + minimumFractionDigits: 0, + minimumSignificantDigits: 2, + maximumSignificantDigits: 2, + }); +}; export const numberFormatter = new Intl.NumberFormat('en-US', { minimumFractionDigits: 0, diff --git a/plugins/cost-insights/src/utils/graphs.test.ts b/plugins/cost-insights/src/utils/graphs.test.ts new file mode 100644 index 0000000000..72749d7682 --- /dev/null +++ b/plugins/cost-insights/src/utils/graphs.test.ts @@ -0,0 +1,47 @@ +/* + * 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 { formatGraphValue, tooltipItemOf } from './graphs'; +import { DataKey } from '../types'; +import { createCurrencyFormat } from './currency'; + +describe('graphs', () => { + it('formatGraphValue', () => { + expect(formatGraphValue(createCurrencyFormat('SEK'))(1000, 0)).toEqual( + 'SEK 1,000', + ); + expect(formatGraphValue(createCurrencyFormat('EUR'))(1000, 0)).toEqual( + '€1,000', + ); + expect(formatGraphValue(createCurrencyFormat('USD'))(1000, 0)).toEqual( + '$1,000', + ); + }); + it('tooltipItemOf', () => { + expect( + tooltipItemOf(createCurrencyFormat('EUR'))({ + value: '1000', + color: 'red', + dataKey: DataKey.Current, + name: 'Kubernetes', + }), + ).toEqual({ + fill: 'red', + label: 'Kubernetes', + value: '€1,000.00', + }); + }); +}); diff --git a/plugins/cost-insights/src/utils/graphs.ts b/plugins/cost-insights/src/utils/graphs.ts index 172f6cd7e3..7512ec0c51 100644 --- a/plugins/cost-insights/src/utils/graphs.ts +++ b/plugins/cost-insights/src/utils/graphs.ts @@ -16,51 +16,51 @@ import { TooltipProps } from 'recharts'; import { Payload } from 'recharts/types/component/DefaultTooltipContent'; -import { AlertCost, DataKey, Entity, ResourceData } from '../types'; +import { AlertCost, DataKey, ResourceData } from '../types'; +import { Entity } from '@backstage/plugin-cost-insights-common'; import { currencyFormatter, dateFormatter, lengthyCurrencyFormatter, } from './formatters'; -export function formatGraphValue( - value: number, - _index: number, - format?: string, -) { - if (format === 'number') { - return value.toLocaleString(); - } +export const formatGraphValue = + (baseCurrency: Intl.NumberFormat) => + (value: number, _index: number, format?: string) => { + if (format === 'number') { + return value.toLocaleString(); + } - if (value < 1) { - return lengthyCurrencyFormatter.format(value); - } + if (value < 1) { + return lengthyCurrencyFormatter(baseCurrency).format(value); + } - return currencyFormatter.format(value); -} + return currencyFormatter(baseCurrency).format(value); + }; export const overviewGraphTickFormatter = (millis: string | number) => typeof millis === 'number' ? dateFormatter.format(millis) : millis; -export const tooltipItemOf = (payload: Payload) => { - const value = - typeof payload.value === 'number' - ? currencyFormatter.format(payload.value) - : payload.value; - const fill = payload.color as string; +export const tooltipItemOf = + (baseCurrency: Intl.NumberFormat) => (payload: Payload) => { + const value = + payload.value && !isNaN(Number(payload.value)) + ? baseCurrency.format(Number(payload.value)) + : payload.value; + const fill = payload.color as string; - switch (payload.dataKey) { - case DataKey.Current: - case DataKey.Previous: - return { - label: payload.name, - value: value, - fill: fill, - }; - default: - return null; - } -}; + switch (payload.dataKey) { + case DataKey.Current: + case DataKey.Previous: + return { + label: payload.name, + value: value, + fill: fill, + }; + default: + return null; + } + }; export const resourceOf = (entity: Entity | AlertCost): ResourceData => ({ name: entity.id, diff --git a/plugins/cost-insights/src/utils/history.ts b/plugins/cost-insights/src/utils/history.ts index 466f506479..ccdce9c3e4 100644 --- a/plugins/cost-insights/src/utils/history.ts +++ b/plugins/cost-insights/src/utils/history.ts @@ -16,7 +16,8 @@ import qs from 'qs'; import * as yup from 'yup'; -import { Duration, Group, PageFilters } from '../types'; +import { Duration, PageFilters } from '../types'; +import { Group } from '@backstage/plugin-cost-insights-common'; import { getDefaultPageFilters } from '../utils/filters'; import { ConfigContextProps } from '../hooks/useConfig'; diff --git a/plugins/cost-insights/src/utils/loading.ts b/plugins/cost-insights/src/utils/loading.ts index 2a453a58f1..7fe95a39db 100644 --- a/plugins/cost-insights/src/utils/loading.ts +++ b/plugins/cost-insights/src/utils/loading.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Duration, Entity, Loading, Maybe, Product } from '../types'; +import { Duration, Loading } from '../types'; +import { Entity, Maybe, Product } from '@backstage/plugin-cost-insights-common'; import { DEFAULT_DURATION } from './duration'; export type ProductState = { diff --git a/plugins/cost-insights/src/utils/sort.ts b/plugins/cost-insights/src/utils/sort.ts index 817e4b9012..0bdcc388b5 100644 --- a/plugins/cost-insights/src/utils/sort.ts +++ b/plugins/cost-insights/src/utils/sort.ts @@ -13,7 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DateAggregation, ResourceData } from '../types'; +import { ResourceData } from '../types'; +import { DateAggregation } from '@backstage/plugin-cost-insights-common'; import { ProductState } from './loading'; export const aggregationSort = ( diff --git a/plugins/cost-insights/src/utils/sum.ts b/plugins/cost-insights/src/utils/sum.ts index 1ab6bdef71..2aba59891a 100644 --- a/plugins/cost-insights/src/utils/sum.ts +++ b/plugins/cost-insights/src/utils/sum.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DateAggregation } from '../types'; +import { DateAggregation } from '@backstage/plugin-cost-insights-common'; export const aggregationSum = (aggregation: DateAggregation[]) => aggregation.reduce((total, curAgg) => total + curAgg.amount, 0); diff --git a/plugins/dynatrace/CHANGELOG.md b/plugins/dynatrace/CHANGELOG.md index 4ab45e9df6..90ff121044 100644 --- a/plugins/dynatrace/CHANGELOG.md +++ b/plugins/dynatrace/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-dynatrace +## 1.0.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 1.0.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 1.0.1 + +### Patch Changes + +- cbe11d1e23: Tweak README +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 1.0.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 1.0.1-next.0 ### Patch Changes diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json index bdd54bf865..1e0c6e9768 100644 --- a/plugins/dynatrace/package.json +++ b/plugins/dynatrace/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-dynatrace", - "version": "1.0.1-next.0", + "version": "1.0.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -45,7 +45,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "express": "^4.18.1", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx index de04f73e36..6d772efc9b 100644 --- a/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx +++ b/plugins/dynatrace/src/components/Synthetics/SyntheticsLocation/SyntheticsLocation.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { SyntheticsLocation } from './SyntheticsLocation'; import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; @@ -60,6 +61,6 @@ describe('SyntheticsLocation', () => { , ); expect(await rendered.findByText(/__location__/)).toBeInTheDocument(); - expect(await rendered.queryByText(/failed/)).not.toBeInTheDocument(); + expect(rendered.queryByText(/failed/)).not.toBeInTheDocument(); }); }); diff --git a/plugins/events-backend-module-aws-sqs/.eslintrc.js b/plugins/events-backend-module-aws-sqs/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md new file mode 100644 index 0000000000..4783c2bebd --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -0,0 +1,46 @@ +# @backstage/plugin-events-backend-module-aws-sqs + +## 0.1.1-next.1 + +### Patch Changes + +- dd008a10c1: Upgrade to AWS SDK for Javascript v3 +- Updated dependencies + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.1.0 + +### Minor Changes + +- d3ecb2382d: Adds a new module `aws-sqs` for plugin-events-backend. + + The module provides an event publisher `AwsSqsConsumingEventPublisher` + which will allow you to receive events from + an AWS SQS queue and will publish these to the used event broker. + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-tasks@0.3.7 + - @backstage/plugin-events-node@0.1.0 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/config@1.0.4 diff --git a/plugins/events-backend-module-aws-sqs/README.md b/plugins/events-backend-module-aws-sqs/README.md new file mode 100644 index 0000000000..cb3a5045c0 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/README.md @@ -0,0 +1,42 @@ +# events-backend-module-aws-sqs + +Welcome to the `events-backend-module-aws-sqs` backend plugin! + +This plugin is a module for the `events-backend` backend plugin +and extends it with an `AwsSqsConsumingEventPublisher`. + +This event publisher will allow you to receive events from +an AWS SQS queue and will publish these to the used event broker. + +## Configuration + +The polled AWS SQS queues depend on your configuration: + +```yaml +events: + modules: + awsSqs: + awsSqsConsumingEventPublisher: + topics: + topicName1: # replace with actual topic name as expected by subscribers + queue: + url: 'https://sqs.us-east-2.amazonaws.com/123456789012/MyQueue' + region: us-east-2 + # visibilityTimeout - as HumanDuration; defaults to queue-based config + # waitTime - as HumanDuration; defaults to max of 20 seconds (long polling) + # timeout - as HumanDuration; timeout for the task execution + # waitTimeAfterEmptyReceive - as HumanDuration; time to wait before a retry when there was no message. + topicName2: + # [...] +``` + +## Installation + +1. Install the [`events-backend` plugin](../events-backend/README.md). +2. Install this module +3. Add your configuration. + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-events-backend-module-aws-sqs +``` diff --git a/plugins/events-backend-module-aws-sqs/api-report.md b/plugins/events-backend-module-aws-sqs/api-report.md new file mode 100644 index 0000000000..79a422c9f3 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/api-report.md @@ -0,0 +1,29 @@ +## API Report File for "@backstage/plugin-events-backend-module-aws-sqs" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; +import { EventBroker } from '@backstage/plugin-events-node'; +import { EventPublisher } from '@backstage/plugin-events-node'; +import { Logger } from 'winston'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; + +// @public +export class AwsSqsConsumingEventPublisher implements EventPublisher { + // (undocumented) + static fromConfig(env: { + config: Config; + logger: Logger; + scheduler: PluginTaskScheduler; + }): AwsSqsConsumingEventPublisher[]; + // (undocumented) + setEventBroker(eventBroker: EventBroker): Promise; +} + +// @alpha +export const awsSqsConsumingEventPublisherEventsModule: ( + options?: undefined, +) => BackendFeature; +``` diff --git a/plugins/events-backend-module-aws-sqs/config.d.ts b/plugins/events-backend-module-aws-sqs/config.d.ts new file mode 100644 index 0000000000..dfef35bf22 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/config.d.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HumanDuration } from '@backstage/types'; + +export interface Config { + events?: { + modules?: { + /** + * events-backend-module-aws-sqs plugin configuration. + */ + awsSqs?: { + /** + * Configuration for AwsSqsConsumingEventPublisher. + */ + awsSqsConsumingEventPublisher?: { + /** + * Contains a record per topic for which an AWS SQS queue + * should be used as source of events. + */ + topics: Record< + string, + { + /** + * (Required) Queue-related configuration. + */ + queue: { + /** + * (Required) The region of the AWS SQS queue. + */ + region: string; + /** + * (Required) The absolute URL for the AWS SQS queue to be used. + */ + url: string; + /** + * (Optional) Visibility timeout for messages in flight. + */ + visibilityTimeout: HumanDuration; + /** + * (Optional) Wait time when polling for available messages. + * Default: 20 seconds. + */ + waitTime: HumanDuration; + }; + /** + * (Optional) Timeout for the task execution which includes polling for messages + * and publishing the events to the event broker + * and the wait time after empty receives. + * + * Must be greater than `queue.waitTime` + `waitTimeAfterEmptyReceive`. + */ + timeout: HumanDuration; + /** + * (Optional) Wait time before polling again if no message was received. + * Default: 1 minute. + */ + waitTimeAfterEmptyReceive: HumanDuration; + } + >; + }; + }; + }; + }; +} diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json new file mode 100644 index 0000000000..30a2478313 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -0,0 +1,49 @@ +{ + "name": "@backstage/plugin-events-backend-module-aws-sqs", + "version": "0.1.1-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@aws-sdk/client-sqs": "^3.208.0", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", + "@backstage/types": "workspace:^", + "luxon": "^3.0.0", + "winston": "^3.2.1" + }, + "devDependencies": { + "@aws-sdk/types": "^3.208.0", + "@backstage/backend-common": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", + "aws-sdk-client-mock": "^2.0.0" + }, + "files": [ + "alpha", + "config.d.ts", + "dist" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/events-backend-module-aws-sqs/src/index.ts b/plugins/events-backend-module-aws-sqs/src/index.ts new file mode 100644 index 0000000000..73b950856b --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/src/index.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * The module "sqs" for the Backstage backend plugin "events" + * adding an AWS SQS-based publisher, + * receiving events from an AWS SQS queue and passing it to the + * internal event broker. + * + * @packageDocumentation + */ + +export { AwsSqsConsumingEventPublisher } from './publisher/AwsSqsConsumingEventPublisher'; +export { awsSqsConsumingEventPublisherEventsModule } from './service/AwsSqsConsumingEventPublisherEventsModule'; diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts new file mode 100644 index 0000000000..e32245b483 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.test.ts @@ -0,0 +1,227 @@ +/* + * 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 { + DeleteMessageBatchCommand, + ReceiveMessageCommand, + SQSClient, +} from '@aws-sdk/client-sqs'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { ConfigReader } from '@backstage/config'; +import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { mockClient } from 'aws-sdk-client-mock'; +import { AwsSqsConsumingEventPublisher } from './AwsSqsConsumingEventPublisher'; + +describe('AwsSqsConsumingEventPublisher', () => { + it('creates one publisher instance per configured topic', async () => { + const config = new ConfigReader({ + events: { + modules: { + awsSqs: { + awsSqsConsumingEventPublisher: { + topics: { + fake1: { + queue: { + region: 'eu-west-1', + url: 'https://fake1.queue.url', + }, + }, + fake2: { + queue: { + region: 'us-east-1', + url: 'https://fake2.queue.url', + }, + }, + }, + }, + }, + }, + }, + }); + const logger = getVoidLogger(); + const scheduler = { + scheduleTask: jest.fn(), + } as unknown as PluginTaskScheduler; + + const publishers = AwsSqsConsumingEventPublisher.fromConfig({ + config, + logger, + scheduler, + }); + expect(publishers.length).toEqual(2); + }); + + it('polling will be scheduled after connecting to the EventBroker', async () => { + const config = new ConfigReader({ + events: { + modules: { + awsSqs: { + awsSqsConsumingEventPublisher: { + topics: { + fake1: { + queue: { + region: 'eu-west-1', + url: 'https://fake1.queue.url', + }, + }, + }, + }, + }, + }, + }, + }); + const logger = getVoidLogger(); + const scheduler = { + scheduleTask: jest.fn(), + } as unknown as PluginTaskScheduler; + + const publishers = AwsSqsConsumingEventPublisher.fromConfig({ + config, + logger, + scheduler, + }); + expect(publishers.length).toEqual(1); + + const publisher = publishers[0]; + + const eventBroker = new TestEventBroker(); + await publisher.setEventBroker(eventBroker); + + // publisher.connect(..) was causing the polling for events to be scheduled + expect(scheduler.scheduleTask).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'events.awsSqs.publisher:fake1', + frequency: { seconds: 0 }, + timeout: { seconds: 260 }, + scope: 'local', + }), + ); + }); + + it('publishes events for received messages and deletes them in bulk', async () => { + const config = new ConfigReader({ + events: { + modules: { + awsSqs: { + awsSqsConsumingEventPublisher: { + topics: { + fake1: { + queue: { + region: 'eu-west-1', + url: 'https://fake1.queue.url', + }, + waitTimeAfterEmptyReceive: { seconds: 1 }, + }, + }, + }, + }, + }, + }, + }); + const logger = getVoidLogger(); + let taskFn: (() => Promise) | undefined = undefined; + const scheduler = { + scheduleTask: (spec: { fn: () => Promise }) => { + taskFn = spec.fn; + }, + } as unknown as PluginTaskScheduler; + + // on the first attempt, we will return 1 message and 0 messages afterwards + const sqsMock = mockClient(SQSClient); + sqsMock + .on(ReceiveMessageCommand, { + MaxNumberOfMessages: 10, + QueueUrl: 'https://fake1.queue.url', + WaitTimeSeconds: 20, + }) + .resolvesOnce({ + Messages: [], + }) + .resolvesOnce({ + Messages: [ + { + Body: '{"event":"payload1"}', + ReceiptHandle: 'fake-handle1', + MessageAttributes: { + 'X-Custom-Attr': { + DataType: 'String', + StringValue: 'value', + }, + }, + }, + { + Body: '{"event":"payload2"}', + ReceiptHandle: 'fake-handle2', + }, + ], + }) + .on(DeleteMessageBatchCommand, { + Entries: [ + { + Id: 'message-0', + ReceiptHandle: 'fake-handle1', + }, + { + Id: 'message-1', + ReceiptHandle: 'fake-handle2', + }, + ], + QueueUrl: 'https://fake1.queue.url', + }) + .resolvesOnce({ + Failed: [ + { + Id: 'message-1', + Message: 'test failure', + SenderFault: true, + Code: '400', + }, + ], + Successful: [{ Id: 'message-0' }], + }); + + const publishers = AwsSqsConsumingEventPublisher.fromConfig({ + config, + logger, + scheduler, + }); + expect(publishers.length).toEqual(1); + const publisher = publishers[0]; + + const eventBroker = new TestEventBroker(); + await publisher.setEventBroker(eventBroker); + + await taskFn!(); + await taskFn!(); + await taskFn!(); + + expect(eventBroker.published.length).toEqual(2); + expect(eventBroker.published[0].topic).toEqual('fake1'); + expect(eventBroker.published[0].eventPayload).toEqual({ + event: 'payload1', + }); + expect(eventBroker.published[0].metadata).toEqual({ + 'X-Custom-Attr': 'value', + }); + + expect(eventBroker.published[1].topic).toEqual('fake1'); + expect(eventBroker.published[1].eventPayload).toEqual({ + event: 'payload2', + }); + expect(eventBroker.published[1].metadata).toEqual({}); + }); +}); diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts new file mode 100644 index 0000000000..4638769c45 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts @@ -0,0 +1,191 @@ +/* + * 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 { + DeleteMessageBatchCommand, + Message, + ReceiveMessageCommand, + ReceiveMessageCommandInput, + SQSClient, +} from '@aws-sdk/client-sqs'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { Config } from '@backstage/config'; +import { EventBroker, EventPublisher } from '@backstage/plugin-events-node'; +import { Logger } from 'winston'; +import { AwsSqsEventSourceConfig, readConfig } from './config'; + +/** + * Publishes events received from an AWS SQS queue. + * The message payload will be used as event payload and passed to registered subscribers. + * + * @public + */ +// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) +export class AwsSqsConsumingEventPublisher implements EventPublisher { + private readonly topic: string; + private readonly receiveParams: ReceiveMessageCommandInput; + private readonly sqs: SQSClient; + private readonly queueUrl: string; + private readonly taskTimeoutSeconds: number; + private readonly waitTimeAfterEmptyReceiveMs; + private eventBroker?: EventBroker; + + static fromConfig(env: { + config: Config; + logger: Logger; + scheduler: PluginTaskScheduler; + }): AwsSqsConsumingEventPublisher[] { + return readConfig(env.config).map( + config => + new AwsSqsConsumingEventPublisher(env.logger, env.scheduler, config), + ); + } + + private constructor( + private readonly logger: Logger, + private readonly scheduler: PluginTaskScheduler, + config: AwsSqsEventSourceConfig, + ) { + this.topic = config.topic; + + this.receiveParams = { + MaxNumberOfMessages: 10, + MessageAttributeNames: ['All'], + QueueUrl: config.queueUrl, + VisibilityTimeout: config.visibilityTimeout?.as('seconds'), + WaitTimeSeconds: config.pollingWaitTime.as('seconds'), + }; + this.sqs = new SQSClient({ region: config.region }); + this.queueUrl = config.queueUrl; + + this.taskTimeoutSeconds = config.timeout.as('seconds'); + this.waitTimeAfterEmptyReceiveMs = + config.waitTimeAfterEmptyReceive.as('milliseconds'); + } + + async setEventBroker(eventBroker: EventBroker): Promise { + this.eventBroker = eventBroker; + return this.start(); + } + + private async start(): Promise { + const id = `events.awsSqs.publisher:${this.topic}`; + const logger = this.logger.child({ + class: AwsSqsConsumingEventPublisher.prototype.constructor.name, + taskId: id, + }); + + await this.scheduler.scheduleTask({ + id: id, + frequency: { seconds: 0 }, + timeout: { seconds: this.taskTimeoutSeconds }, + scope: 'local', + fn: async () => { + try { + const numMessages = await this.consumeMessages(); + if (numMessages === 0) { + await this.sleep(this.waitTimeAfterEmptyReceiveMs); + } + } catch (error) { + logger.error(error); + } + }, + }); + } + + private async deleteMessages(messages?: Message[]): Promise { + if (!messages) { + return; + } + + const deleteParams = { + QueueUrl: this.queueUrl, + Entries: messages.map((message, index) => { + return { + Id: message.MessageId ?? `message-${index}`, + ReceiptHandle: message.ReceiptHandle, + }; + }), + }; + + try { + const result = await this.sqs.send( + new DeleteMessageBatchCommand(deleteParams), + ); + if (result.Failed) { + this.logger.error( + `Failed to delete ${result.Failed!.length} of ${ + messages.length + } messages from AWS SQS ${this.queueUrl}. First: ${ + result.Failed[0].Message + }`, + ); + } + } catch (error) { + this.logger.error( + `Failed to delete message from AWS SQS ${this.queueUrl}`, + error, + ); + } + } + + private async consumeMessages(): Promise { + try { + const data = await this.sqs.send( + new ReceiveMessageCommand(this.receiveParams), + ); + + data.Messages?.forEach(message => { + const eventPayload = JSON.parse(message.Body!); + + const metadata: Record = {}; + Object.keys(message.MessageAttributes ?? {}).forEach(key => { + const attrValue = message.MessageAttributes![key]; + if ( + !attrValue || + !attrValue.DataType || + !['String', 'Number'].includes(attrValue.DataType) + ) { + return; + } + + const value = attrValue.StringListValues ?? attrValue.StringValue; + if (value !== undefined) { + metadata[key] = value; + } + }); + + this.eventBroker!.publish({ + topic: this.topic, + eventPayload, + metadata, + }); + }); + await this.deleteMessages(data.Messages); + return data.Messages?.length ?? 0; + } catch (error) { + this.logger.error( + `Failed to receive events from AWS SQS ${this.queueUrl}`, + error, + ); + return 0; + } + } + + private sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts b/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts new file mode 100644 index 0000000000..1fa9df4ce3 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/src/publisher/config.test.ts @@ -0,0 +1,222 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { readConfig } from './config'; + +describe('readConfig', () => { + it('not configured', () => { + const config = new ConfigReader({}); + + const publisherConfigs = readConfig(config); + + expect(publisherConfigs.length).toBe(0); + }); + + it('only required fields configured', () => { + const config = new ConfigReader({ + events: { + modules: { + awsSqs: { + awsSqsConsumingEventPublisher: { + topics: { + fake1: { + queue: { + region: 'eu-west-1', + url: 'https://fake1.queue.url', + }, + }, + fake2: { + queue: { + region: 'us-east-1', + url: 'https://fake2.queue.url', + }, + }, + }, + }, + }, + }, + }, + }); + + const publisherConfigs = readConfig(config); + + expect(publisherConfigs.length).toBe(2); + + expect(publisherConfigs[0].topic).toEqual('fake1'); + expect(publisherConfigs[0].region).toEqual('eu-west-1'); + expect(publisherConfigs[0].queueUrl).toEqual('https://fake1.queue.url'); + expect(publisherConfigs[0].pollingWaitTime.as('seconds')).toBe(20); + expect(publisherConfigs[0].timeout.as('seconds')).toBe(260); + expect(publisherConfigs[0].waitTimeAfterEmptyReceive.as('seconds')).toBe( + 60, + ); + }); + + it('all fields configured', () => { + const config = new ConfigReader({ + events: { + modules: { + awsSqs: { + awsSqsConsumingEventPublisher: { + topics: { + fake1: { + queue: { + region: 'eu-west-1', + url: 'https://fake1.queue.url', + visibilityTimeout: { minutes: 5 }, + waitTime: { seconds: 10 }, + }, + timeout: { minutes: 5 }, + waitTimeAfterEmptyReceive: { seconds: 30 }, + }, + }, + }, + }, + }, + }, + }); + + const publisherConfigs = readConfig(config); + + expect(publisherConfigs.length).toBe(1); + + expect(publisherConfigs[0].topic).toEqual('fake1'); + expect(publisherConfigs[0].region).toEqual('eu-west-1'); + expect(publisherConfigs[0].queueUrl).toEqual('https://fake1.queue.url'); + expect(publisherConfigs[0].pollingWaitTime.as('seconds')).toBe(10); + expect(publisherConfigs[0].timeout.as('seconds')).toBe(300); + expect(publisherConfigs[0].waitTimeAfterEmptyReceive.as('seconds')).toBe( + 30, + ); + }); + + it('fail on negative queue.waitTime', () => { + const config = new ConfigReader({ + events: { + modules: { + awsSqs: { + awsSqsConsumingEventPublisher: { + topics: { + fake1: { + queue: { + region: 'eu-west-1', + url: 'https://fake1.queue.url', + visibilityTimeout: { minutes: 5 }, + waitTime: { seconds: -10 }, + }, + timeout: { minutes: 5 }, + waitTimeAfterEmptyReceive: { seconds: 30 }, + }, + }, + }, + }, + }, + }, + }); + + expect(() => readConfig(config)).toThrow( + 'events.modules.awsSqs.awsSqsConsumingEventPublisher.topics.fake1.queue.waitTime must be within 0..20 seconds', + ); + }); + + it('fail on too high queue.waitTime', () => { + const config = new ConfigReader({ + events: { + modules: { + awsSqs: { + awsSqsConsumingEventPublisher: { + topics: { + fake1: { + queue: { + region: 'eu-west-1', + url: 'https://fake1.queue.url', + visibilityTimeout: { minutes: 5 }, + waitTime: { seconds: 30 }, + }, + timeout: { minutes: 5 }, + waitTimeAfterEmptyReceive: { seconds: 30 }, + }, + }, + }, + }, + }, + }, + }); + + expect(() => readConfig(config)).toThrow( + 'events.modules.awsSqs.awsSqsConsumingEventPublisher.topics.fake1.queue.waitTime must be within 0..20 seconds', + ); + }); + + it('fail on too low timeout', () => { + const config = new ConfigReader({ + events: { + modules: { + awsSqs: { + awsSqsConsumingEventPublisher: { + topics: { + fake1: { + queue: { + region: 'eu-west-1', + url: 'https://fake1.queue.url', + visibilityTimeout: { minutes: 5 }, + waitTime: { seconds: 10 }, + }, + timeout: { seconds: 10 }, + waitTimeAfterEmptyReceive: { seconds: 30 }, + }, + }, + }, + }, + }, + }, + }); + + expect(() => readConfig(config)).toThrow( + 'The events.modules.awsSqs.awsSqsConsumingEventPublisher.topics.fake1.timeout must be greater than events.modules.awsSqs.awsSqsConsumingEventPublisher.topics.fake1.queue.waitTime', + ); + }); + + it('fail on negative waitTimeAfterEmptyReceive', () => { + const config = new ConfigReader({ + events: { + modules: { + awsSqs: { + awsSqsConsumingEventPublisher: { + topics: { + fake1: { + queue: { + region: 'eu-west-1', + url: 'https://fake1.queue.url', + visibilityTimeout: { minutes: 5 }, + waitTime: { seconds: 10 }, + }, + timeout: { minutes: 5 }, + waitTimeAfterEmptyReceive: { seconds: -30 }, + }, + }, + }, + }, + }, + }, + }); + + expect(() => readConfig(config)).toThrow( + 'The events.modules.awsSqs.awsSqsConsumingEventPublisher.topics.fake1.waitTimeAfterEmptyReceive must not be negative', + ); + }); +}); diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/config.ts b/plugins/events-backend-module-aws-sqs/src/publisher/config.ts new file mode 100644 index 0000000000..c6b74ea8d3 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/src/publisher/config.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { HumanDuration, JsonObject } from '@backstage/types'; +import { Duration } from 'luxon'; + +const CONFIG_PREFIX_MODULE = 'events.modules.awsSqs.'; +const CONFIG_PREFIX_PUBLISHER = `${CONFIG_PREFIX_MODULE}awsSqsConsumingEventPublisher.`; +const DEFAULT_WAIT_TIME_AFTER_EMPTY_RECEIVE = { minutes: 1 }; +const MAX_WAIT_SECONDS = 20; + +export interface AwsSqsEventSourceConfig { + pollingWaitTime: Duration; + queueUrl: string; + region: string; + timeout: Duration; + topic: string; + visibilityTimeout?: Duration; + waitTimeAfterEmptyReceive: Duration; +} + +// TODO(pjungermann): validation could be improved similar to `convertToHumanDuration` at @backstage/backend-tasks +function readOptionalHumanDuration( + config: Config, + key: string, +): HumanDuration | undefined { + return config.getOptional(key) as HumanDuration; +} + +function readOptionalDuration( + config: Config, + key: string, +): Duration | undefined { + const duration = readOptionalHumanDuration(config, key); + return duration ? Duration.fromObject(duration) : undefined; +} + +export function readConfig(config: Config): AwsSqsEventSourceConfig[] { + const key = `${CONFIG_PREFIX_PUBLISHER}topics`; + const topics = config.getOptionalConfig(key); + + return ( + topics?.keys()?.map(topic => { + const topicConfig = topics.getConfig(topic); + const keyPrefix = `${key}.${topic}.`; + + // queue config: + const pollingWaitTime = Duration.fromObject( + readOptionalHumanDuration(topicConfig, 'queue.waitTime') ?? { + seconds: MAX_WAIT_SECONDS, + }, + ); + if ( + pollingWaitTime.valueOf() < 0 || + pollingWaitTime.as('seconds') > MAX_WAIT_SECONDS + ) { + throw new Error( + `${keyPrefix}queue.waitTime must be within 0..${MAX_WAIT_SECONDS} seconds.`, + ); + } + const queueUrl = topicConfig.getString('queue.url'); + const region = topicConfig.getString('queue.region'); + const visibilityTimeout = readOptionalDuration( + topicConfig, + 'queue.visibilityTimeout', + ); + + // task: + const waitTimeAfterEmptyReceive = Duration.fromObject( + readOptionalHumanDuration(topicConfig, 'waitTimeAfterEmptyReceive') ?? + DEFAULT_WAIT_TIME_AFTER_EMPTY_RECEIVE, + ); + if (waitTimeAfterEmptyReceive.valueOf() < 0) { + throw new Error( + `The ${keyPrefix}waitTimeAfterEmptyReceive must not be negative.`, + ); + } + const timeout = + readOptionalDuration(topicConfig, 'timeout') ?? + pollingWaitTime + .plus(waitTimeAfterEmptyReceive) + .plus(Duration.fromObject({ seconds: 180 })); + if ( + timeout.valueOf() <= + pollingWaitTime.valueOf() + waitTimeAfterEmptyReceive.valueOf() + ) { + throw new Error( + `The ${keyPrefix}timeout must be greater than ${keyPrefix}queue.waitTime + ${keyPrefix}waitTimeAfterEmptyReceive.`, + ); + } + + return { + pollingWaitTime, + queueUrl, + region, + timeout, + topic, + visibilityTimeout, + waitTimeAfterEmptyReceive, + }; + }) ?? [] + ); +} diff --git a/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.test.ts b/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.test.ts new file mode 100644 index 0000000000..589b8c93fa --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { coreServices } from '@backstage/backend-plugin-api'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { awsSqsConsumingEventPublisherEventsModule } from './AwsSqsConsumingEventPublisherEventsModule'; +import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher'; + +describe('awsSqsEventsModule', () => { + it('should be correctly wired and set up', async () => { + const config = new ConfigReader({ + events: { + modules: { + awsSqs: { + awsSqsConsumingEventPublisher: { + topics: { + fake1: { + queue: { + region: 'eu-west-1', + url: 'https://fake1.queue.url', + }, + }, + fake2: { + queue: { + region: 'us-east-1', + url: 'https://fake2.queue.url', + }, + }, + }, + }, + }, + }, + }, + }); + + let addedPublishers: AwsSqsConsumingEventPublisher[] | undefined; + const extensionPoint = { + addPublishers: (publishers: any) => { + addedPublishers = publishers; + }, + }; + + const scheduler = { + scheduleTask: jest.fn(), + }; + + await startTestBackend({ + extensionPoints: [[eventsExtensionPoint, extensionPoint]], + services: [ + [coreServices.config, config], + [coreServices.logger, getVoidLogger()], + [coreServices.scheduler, scheduler], + ], + features: [awsSqsConsumingEventPublisherEventsModule()], + }); + + expect(addedPublishers).not.toBeUndefined(); + expect(addedPublishers!.length).toEqual(2); + + const eventBroker = new TestEventBroker(); + await Promise.all( + addedPublishers!.map(publisher => publisher.setEventBroker(eventBroker)), + ); + + // publisher.connect(..) was causing the polling for events to be scheduled + expect(scheduler.scheduleTask).toHaveBeenCalledWith( + expect.objectContaining({ id: 'events.awsSqs.publisher:fake1' }), + ); + expect(scheduler.scheduleTask).toHaveBeenCalledWith( + expect.objectContaining({ id: 'events.awsSqs.publisher:fake2' }), + ); + }); +}); diff --git a/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.ts b/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.ts new file mode 100644 index 0000000000..a6c43d4046 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/src/service/AwsSqsConsumingEventPublisherEventsModule.ts @@ -0,0 +1,53 @@ +/* + * 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, + createBackendModule, + loggerToWinstonLogger, +} from '@backstage/backend-plugin-api'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { AwsSqsConsumingEventPublisher } from '../publisher/AwsSqsConsumingEventPublisher'; + +/** + * AWS SQS module for the Events plugin. + * + * @alpha + */ +export const awsSqsConsumingEventPublisherEventsModule = createBackendModule({ + pluginId: 'events', + moduleId: 'awsSqsConsumingEventPublisherEventsModule', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + events: eventsExtensionPoint, + logger: coreServices.logger, + scheduler: coreServices.scheduler, + }, + async init({ config, events, logger, scheduler }) { + const winstonLogger = loggerToWinstonLogger(logger); + const sqs = AwsSqsConsumingEventPublisher.fromConfig({ + config: config, + logger: winstonLogger, + scheduler: scheduler, + }); + + events.addPublishers(sqs); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-aws-sqs/src/setupTests.ts b/plugins/events-backend-module-aws-sqs/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/events-backend-module-aws-sqs/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/events-backend-module-azure/.eslintrc.js b/plugins/events-backend-module-azure/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-backend-module-azure/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md new file mode 100644 index 0000000000..b69fb89f86 --- /dev/null +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -0,0 +1,38 @@ +# @backstage/plugin-events-backend-module-azure + +## 0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## 0.1.0 + +### Minor Changes + +- 12cd94b7e9: Adds a new module `azure` to plugin-events-backend. + + The module adds a new event router `AzureDevOpsEventRouter`. + + The event router will re-publish events received at topic `azureDevOps` + under a more specific topic depending on their `$.eventType` value + (e.g., `azureDevOps.git.push`). + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-azure/README.md. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 diff --git a/plugins/events-backend-module-azure/README.md b/plugins/events-backend-module-azure/README.md new file mode 100644 index 0000000000..d16315b6e7 --- /dev/null +++ b/plugins/events-backend-module-azure/README.md @@ -0,0 +1,43 @@ +# events-backend-module-azure + +Welcome to the `events-backend-module-azure` backend plugin! + +This plugin is a module for the `events-backend` backend plugin +and extends it with an `AzureDevOpsEventRouter`. + +The event router will subscribe to the topic `azureDevOps` +and route the events to more concrete topics based on the value +of the provided `$.eventType` payload field. + +Examples: + +| `$.eventType` | topic | +| ------------------------- | ------------------------------------- | +| `git.push` | `azureDevOps.git.push` | +| `git.pullrequest.created` | `azureDevOps.git.pullrequest.created` | + +Please find all possible webhook event types at the +[official documentation of events](https://learn.microsoft.com/en-us/azure/devops/service-hooks/events?source=recommendations&view=azure-devops) +and [webhooks](https://learn.microsoft.com/en-us/azure/devops/service-hooks/services/webhooks?view=azure-devops). + +## Installation + +Install the [`events-backend` plugin](../events-backend/README.md). + +Install this module: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-events-backend-module-azure +``` + +Add the event router to the `EventsBackend`: + +```diff ++const githubEventRouter = new AzureDevOpsEventRouter(); + + EventsBackend ++ .addPublishers(githubEventRouter) ++ .addSubscribers(githubEventRouter); +// [...] +``` diff --git a/plugins/events-backend-module-azure/api-report.md b/plugins/events-backend-module-azure/api-report.md new file mode 100644 index 0000000000..510ec91c47 --- /dev/null +++ b/plugins/events-backend-module-azure/api-report.md @@ -0,0 +1,21 @@ +## API Report File for "@backstage/plugin-events-backend-module-azure" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { EventParams } from '@backstage/plugin-events-node'; +import { SubTopicEventRouter } from '@backstage/plugin-events-node'; + +// @public +export class AzureDevOpsEventRouter extends SubTopicEventRouter { + constructor(); + // (undocumented) + protected determineSubTopic(params: EventParams): string | undefined; +} + +// @alpha +export const azureDevOpsEventRouterEventsModule: ( + options?: undefined, +) => BackendFeature; +``` diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json new file mode 100644 index 0000000000..9e503e4bb5 --- /dev/null +++ b/plugins/events-backend-module-azure/package.json @@ -0,0 +1,40 @@ +{ + "name": "@backstage/plugin-events-backend-module-azure", + "version": "0.1.1-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", + "supertest": "^6.1.3" + }, + "files": [ + "alpha", + "dist" + ] +} diff --git a/plugins/events-backend-module-azure/src/index.ts b/plugins/events-backend-module-azure/src/index.ts new file mode 100644 index 0000000000..9a83f751f6 --- /dev/null +++ b/plugins/events-backend-module-azure/src/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * The module "azure" for the Backstage backend plugin "events-backend" + * adding an event router for Azure DevOps. + * + * @packageDocumentation + */ + +export { AzureDevOpsEventRouter } from './router/AzureDevOpsEventRouter'; +export { azureDevOpsEventRouterEventsModule } from './service/AzureDevOpsEventRouterEventsModule'; diff --git a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts new file mode 100644 index 0000000000..56e761a8fc --- /dev/null +++ b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { AzureDevOpsEventRouter } from './AzureDevOpsEventRouter'; + +describe('AzureDevOpsEventRouter', () => { + const eventRouter = new AzureDevOpsEventRouter(); + const topic = 'azureDevOps'; + const eventPayload = { eventType: 'test.type', test: 'payload' }; + const metadata = {}; + + it('no $.eventType', () => { + const eventBroker = new TestEventBroker(); + eventRouter.setEventBroker(eventBroker); + + eventRouter.onEvent({ + topic, + eventPayload: { invalid: 'payload' }, + metadata, + }); + + expect(eventBroker.published).toEqual([]); + }); + + it('with $.eventType', () => { + const eventBroker = new TestEventBroker(); + eventRouter.setEventBroker(eventBroker); + + eventRouter.onEvent({ topic, eventPayload, metadata }); + + expect(eventBroker.published.length).toBe(1); + expect(eventBroker.published[0].topic).toEqual('azureDevOps.test.type'); + expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); + expect(eventBroker.published[0].metadata).toEqual(metadata); + }); +}); diff --git a/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts new file mode 100644 index 0000000000..11dd7546dd --- /dev/null +++ b/plugins/events-backend-module-azure/src/router/AzureDevOpsEventRouter.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EventParams, + SubTopicEventRouter, +} from '@backstage/plugin-events-node'; + +/** + * Subscribes to the generic `azureDevOps` topic + * and publishes the events under the more concrete sub-topic + * depending on the `$.eventType` provided. + * + * @public + */ +export class AzureDevOpsEventRouter extends SubTopicEventRouter { + constructor() { + super('azureDevOps'); + } + + protected determineSubTopic(params: EventParams): string | undefined { + if ('eventType' in (params.eventPayload as object)) { + const payload = params.eventPayload as { eventType: string }; + return payload.eventType; + } + + return undefined; + } +} diff --git a/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.test.ts b/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.test.ts new file mode 100644 index 0000000000..86ab7ea17d --- /dev/null +++ b/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { startTestBackend } from '@backstage/backend-test-utils'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { azureDevOpsEventRouterEventsModule } from './AzureDevOpsEventRouterEventsModule'; +import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; + +describe('azureDevOpsEventRouterEventsModule', () => { + it('should be correctly wired and set up', async () => { + let addedPublisher: AzureDevOpsEventRouter | undefined; + let addedSubscriber: AzureDevOpsEventRouter | undefined; + const extensionPoint = { + addPublishers: (publisher: any) => { + addedPublisher = publisher; + }, + addSubscribers: (subscriber: any) => { + addedSubscriber = subscriber; + }, + }; + + await startTestBackend({ + extensionPoints: [[eventsExtensionPoint, extensionPoint]], + services: [], + features: [azureDevOpsEventRouterEventsModule()], + }); + + expect(addedPublisher).not.toBeUndefined(); + expect(addedPublisher).toBeInstanceOf(AzureDevOpsEventRouter); + expect(addedSubscriber).not.toBeUndefined(); + expect(addedSubscriber).toBeInstanceOf(AzureDevOpsEventRouter); + }); +}); diff --git a/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.ts b/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.ts new file mode 100644 index 0000000000..1219452bd8 --- /dev/null +++ b/plugins/events-backend-module-azure/src/service/AzureDevOpsEventRouterEventsModule.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { AzureDevOpsEventRouter } from '../router/AzureDevOpsEventRouter'; + +/** + * Module for the events-backend plugin, adding an event router for Azure DevOps. + * + * Registers the {@link AzureDevOpsEventRouter}. + * + * @alpha + */ +export const azureDevOpsEventRouterEventsModule = createBackendModule({ + pluginId: 'events', + moduleId: 'azureDevOpsEventRouter', + register(env) { + env.registerInit({ + deps: { + events: eventsExtensionPoint, + }, + async init({ events }) { + const eventRouter = new AzureDevOpsEventRouter(); + + events.addPublishers(eventRouter); + events.addSubscribers(eventRouter); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-azure/src/setupTests.ts b/plugins/events-backend-module-azure/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/events-backend-module-azure/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/events-backend-module-bitbucket-cloud/.eslintrc.js b/plugins/events-backend-module-bitbucket-cloud/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md new file mode 100644 index 0000000000..586f961391 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -0,0 +1,38 @@ +# @backstage/plugin-events-backend-module-bitbucket-cloud + +## 0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## 0.1.0 + +### Minor Changes + +- 6bc121bf0d: Adds a new module `bitbucket-cloud` to plugin-events-backend. + + The module adds a new event router `BitbucketCloudEventRouter`. + + The event router will re-publish events received at topic `bitbucketCloud` + under a more specific topic depending on their `x-event-key` value + (e.g., `bitbucketCloud.repo:push`). + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-bitbucket-cloud/README.md. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 diff --git a/plugins/events-backend-module-bitbucket-cloud/README.md b/plugins/events-backend-module-bitbucket-cloud/README.md new file mode 100644 index 0000000000..c58529ed7a --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/README.md @@ -0,0 +1,43 @@ +# events-backend-module-bitbucket-cloud + +Welcome to the `events-backend-module-bitbucket-cloud` backend plugin! + +This plugin is a module for the `events-backend` backend plugin +and extends it with an `BitbucketCloudEventRouter`. + +The event router will subscribe to the topic `bitbucketCloud` +and route the events to more concrete topics based on the value +of the provided `x-event-key` metadata field. + +Examples: + +| x-event-key | topic | +| --------------------- | ------------------------------------ | +| `repo:push` | `bitbucketCloud.repo:push` | +| `repo:updated` | `bitbucketCloud.repo:updated` | +| `pullrequest:created` | `bitbucketCloud.pullrequest:created` | + +Please find all possible webhook event types at the +[official documentation](https://support.atlassian.com/bitbucket-cloud/docs/event-payloads/). + +## Installation + +Install the [`events-backend` plugin](../events-backend/README.md). + +Install this module: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-events-backend-module-bitbucket-cloud +``` + +Add the event router to the `EventsBackend`: + +```diff ++const bitbucketCloudEventRouter = new BitbucketCloudEventRouter(); + + EventsBackend ++ .addPublishers(bitbucketCloudEventRouter) ++ .addSubscribers(bitbucketCloudEventRouter); +// [...] +``` diff --git a/plugins/events-backend-module-bitbucket-cloud/api-report.md b/plugins/events-backend-module-bitbucket-cloud/api-report.md new file mode 100644 index 0000000000..0e4acfac91 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/api-report.md @@ -0,0 +1,21 @@ +## API Report File for "@backstage/plugin-events-backend-module-bitbucket-cloud" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { EventParams } from '@backstage/plugin-events-node'; +import { SubTopicEventRouter } from '@backstage/plugin-events-node'; + +// @public +export class BitbucketCloudEventRouter extends SubTopicEventRouter { + constructor(); + // (undocumented) + protected determineSubTopic(params: EventParams): string | undefined; +} + +// @alpha +export const bitbucketCloudEventRouterEventsModule: ( + options?: undefined, +) => BackendFeature; +``` diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json new file mode 100644 index 0000000000..016bf47856 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -0,0 +1,40 @@ +{ + "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", + "version": "0.1.1-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", + "supertest": "^6.1.3" + }, + "files": [ + "alpha", + "dist" + ] +} diff --git a/plugins/events-backend-module-bitbucket-cloud/src/index.ts b/plugins/events-backend-module-bitbucket-cloud/src/index.ts new file mode 100644 index 0000000000..0a58212de2 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/src/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * The module "bitbucket-cloud" for the Backstage backend plugin "events-backend" + * adding an event router for Bitbucket Cloud. + * + * @packageDocumentation + */ + +export { BitbucketCloudEventRouter } from './router/BitbucketCloudEventRouter'; +export { bitbucketCloudEventRouterEventsModule } from './service/BitbucketCloudEventRouterEventsModule'; diff --git a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts new file mode 100644 index 0000000000..b7a47984e4 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { BitbucketCloudEventRouter } from './BitbucketCloudEventRouter'; + +describe('BitbucketCloudEventRouter', () => { + const eventRouter = new BitbucketCloudEventRouter(); + const topic = 'bitbucketCloud'; + const eventPayload = { test: 'payload' }; + const metadata = { 'x-event-key': 'test:type' }; + + it('no x-event-key', () => { + const eventBroker = new TestEventBroker(); + eventRouter.setEventBroker(eventBroker); + + eventRouter.onEvent({ topic, eventPayload }); + + expect(eventBroker.published).toEqual([]); + }); + + it('with x-event-key', () => { + const eventBroker = new TestEventBroker(); + eventRouter.setEventBroker(eventBroker); + + eventRouter.onEvent({ topic, eventPayload, metadata }); + + expect(eventBroker.published.length).toBe(1); + expect(eventBroker.published[0].topic).toEqual('bitbucketCloud.test:type'); + expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); + expect(eventBroker.published[0].metadata).toEqual(metadata); + }); +}); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts new file mode 100644 index 0000000000..8350511d65 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/src/router/BitbucketCloudEventRouter.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EventParams, + SubTopicEventRouter, +} from '@backstage/plugin-events-node'; + +/** + * Subscribes to the generic `bitbucketCloud` topic + * and publishes the events under the more concrete sub-topic + * depending on the `x-event-key` provided. + * + * @public + */ +export class BitbucketCloudEventRouter extends SubTopicEventRouter { + constructor() { + super('bitbucketCloud'); + } + + protected determineSubTopic(params: EventParams): string | undefined { + return params.metadata?.['x-event-key'] as string | undefined; + } +} diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.test.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.test.ts new file mode 100644 index 0000000000..e456607a2b --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { startTestBackend } from '@backstage/backend-test-utils'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { bitbucketCloudEventRouterEventsModule } from './BitbucketCloudEventRouterEventsModule'; +import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; + +describe('bitbucketCloudEventRouterEventsModule', () => { + it('should be correctly wired and set up', async () => { + let addedPublisher: BitbucketCloudEventRouter | undefined; + let addedSubscriber: BitbucketCloudEventRouter | undefined; + const extensionPoint = { + addPublishers: (publisher: any) => { + addedPublisher = publisher; + }, + addSubscribers: (subscriber: any) => { + addedSubscriber = subscriber; + }, + }; + + await startTestBackend({ + extensionPoints: [[eventsExtensionPoint, extensionPoint]], + services: [], + features: [bitbucketCloudEventRouterEventsModule()], + }); + + expect(addedPublisher).not.toBeUndefined(); + expect(addedPublisher).toBeInstanceOf(BitbucketCloudEventRouter); + expect(addedSubscriber).not.toBeUndefined(); + expect(addedSubscriber).toBeInstanceOf(BitbucketCloudEventRouter); + }); +}); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.ts b/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.ts new file mode 100644 index 0000000000..7f755a28f4 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/src/service/BitbucketCloudEventRouterEventsModule.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { BitbucketCloudEventRouter } from '../router/BitbucketCloudEventRouter'; + +/** + * Module for the events-backend plugin, adding an event router for Bitbucket Cloud. + * + * Registers the {@link BitbucketCloudEventRouter}. + * + * @alpha + */ +export const bitbucketCloudEventRouterEventsModule = createBackendModule({ + pluginId: 'events', + moduleId: 'bitbucketCloudEventRouter', + register(env) { + env.registerInit({ + deps: { + events: eventsExtensionPoint, + }, + async init({ events }) { + const eventRouter = new BitbucketCloudEventRouter(); + + events.addPublishers(eventRouter); + events.addSubscribers(eventRouter); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-bitbucket-cloud/src/setupTests.ts b/plugins/events-backend-module-bitbucket-cloud/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/events-backend-module-bitbucket-cloud/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/events-backend-module-gerrit/.eslintrc.js b/plugins/events-backend-module-gerrit/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-backend-module-gerrit/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md new file mode 100644 index 0000000000..51f72d3caa --- /dev/null +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -0,0 +1,38 @@ +# @backstage/plugin-events-backend-module-gerrit + +## 0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## 0.1.0 + +### Minor Changes + +- 25f6d7bddb: Adds a new module `gerrit` to plugin-events-backend. + + The module adds a new event router `GerritEventRouter`. + + The event router will re-publish events received at topic `gerrit` + under a more specific topic depending on their `$.type` value + (e.g., `gerrit.change-merged`). + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-gerrit/README.md. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 diff --git a/plugins/events-backend-module-gerrit/README.md b/plugins/events-backend-module-gerrit/README.md new file mode 100644 index 0000000000..933b7a172f --- /dev/null +++ b/plugins/events-backend-module-gerrit/README.md @@ -0,0 +1,42 @@ +# events-backend-module-gerrit + +Welcome to the `events-backend-module-gerrit` backend plugin! + +This plugin is a module for the `events-backend` backend plugin +and extends it with an `GerritEventRouter`. + +The event router will subscribe to the topic `gerrit` +and route the events to more concrete topics based on the value +of the provided `$.type` payload field. + +Examples: + +| `$.type` | topic | +| ---------------- | ----------------------- | +| `change-created` | `gerrit.change-created` | +| `change-merged` | `gerrit.change-merged` | + +Please find all possible webhook event types at the +[official documentation](https://gerrit-review.googlesource.com/Documentation/cmd-stream-events.html#events). + +## Installation + +Install the [`events-backend` plugin](../events-backend/README.md). + +Install this module: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-events-backend-module-gerrit +``` + +Add the event router to the `EventsBackend`: + +```diff ++const gerritEventRouter = new GerritEventRouter(); + + EventsBackend ++ .addPublishers(gerritEventRouter) ++ .addSubscribers(gerritEventRouter); +// [...] +``` diff --git a/plugins/events-backend-module-gerrit/api-report.md b/plugins/events-backend-module-gerrit/api-report.md new file mode 100644 index 0000000000..268e5970fa --- /dev/null +++ b/plugins/events-backend-module-gerrit/api-report.md @@ -0,0 +1,21 @@ +## API Report File for "@backstage/plugin-events-backend-module-gerrit" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { EventParams } from '@backstage/plugin-events-node'; +import { SubTopicEventRouter } from '@backstage/plugin-events-node'; + +// @public +export class GerritEventRouter extends SubTopicEventRouter { + constructor(); + // (undocumented) + protected determineSubTopic(params: EventParams): string | undefined; +} + +// @alpha +export const gerritEventRouterEventsModule: ( + options?: undefined, +) => BackendFeature; +``` diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json new file mode 100644 index 0000000000..a9d655fd7c --- /dev/null +++ b/plugins/events-backend-module-gerrit/package.json @@ -0,0 +1,40 @@ +{ + "name": "@backstage/plugin-events-backend-module-gerrit", + "version": "0.1.1-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", + "supertest": "^6.1.3" + }, + "files": [ + "alpha", + "dist" + ] +} diff --git a/plugins/events-backend-module-gerrit/src/index.ts b/plugins/events-backend-module-gerrit/src/index.ts new file mode 100644 index 0000000000..1a9ebb03ee --- /dev/null +++ b/plugins/events-backend-module-gerrit/src/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/** + * The module `gerrit` for the Backstage backend plugin "events-backend" + * adding an event router for Gerrit. + * + * @packageDocumentation + */ + +export { GerritEventRouter } from './router/GerritEventRouter'; +export { gerritEventRouterEventsModule } from './service/GerritEventRouterEventsModule'; diff --git a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts new file mode 100644 index 0000000000..7302a26012 --- /dev/null +++ b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { GerritEventRouter } from './GerritEventRouter'; + +describe('GerritEventRouter', () => { + const eventRouter = new GerritEventRouter(); + const topic = 'gerrit'; + const eventPayload = { type: 'test-type', test: 'payload' }; + const metadata = {}; + + it('no $.type', () => { + const eventBroker = new TestEventBroker(); + eventRouter.setEventBroker(eventBroker); + + eventRouter.onEvent({ + topic, + eventPayload: { invalid: 'payload' }, + metadata, + }); + + expect(eventBroker.published).toEqual([]); + }); + + it('with $.type', () => { + const eventBroker = new TestEventBroker(); + eventRouter.setEventBroker(eventBroker); + + eventRouter.onEvent({ topic, eventPayload, metadata }); + + expect(eventBroker.published.length).toBe(1); + expect(eventBroker.published[0].topic).toEqual('gerrit.test-type'); + expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); + expect(eventBroker.published[0].metadata).toEqual(metadata); + }); +}); diff --git a/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts new file mode 100644 index 0000000000..3d97508b62 --- /dev/null +++ b/plugins/events-backend-module-gerrit/src/router/GerritEventRouter.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EventParams, + SubTopicEventRouter, +} from '@backstage/plugin-events-node'; + +/** + * Subscribes to the generic `gerrit` topic + * and publishes the events under the more concrete sub-topic + * depending on the `$.type` field provided. + * + * @public + */ +export class GerritEventRouter extends SubTopicEventRouter { + constructor() { + super('gerrit'); + } + + protected determineSubTopic(params: EventParams): string | undefined { + if ('type' in (params.eventPayload as object)) { + const payload = params.eventPayload as { type: string }; + return payload.type; + } + + return undefined; + } +} diff --git a/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.test.ts b/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.test.ts new file mode 100644 index 0000000000..b5fd6a80ca --- /dev/null +++ b/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { startTestBackend } from '@backstage/backend-test-utils'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { gerritEventRouterEventsModule } from './GerritEventRouterEventsModule'; +import { GerritEventRouter } from '../router/GerritEventRouter'; + +describe('gerritEventRouterEventsModule', () => { + it('should be correctly wired and set up', async () => { + let addedPublisher: GerritEventRouter | undefined; + let addedSubscriber: GerritEventRouter | undefined; + const extensionPoint = { + addPublishers: (publisher: any) => { + addedPublisher = publisher; + }, + addSubscribers: (subscriber: any) => { + addedSubscriber = subscriber; + }, + }; + + await startTestBackend({ + extensionPoints: [[eventsExtensionPoint, extensionPoint]], + services: [], + features: [gerritEventRouterEventsModule()], + }); + + expect(addedPublisher).not.toBeUndefined(); + expect(addedPublisher).toBeInstanceOf(GerritEventRouter); + expect(addedSubscriber).not.toBeUndefined(); + expect(addedSubscriber).toBeInstanceOf(GerritEventRouter); + }); +}); diff --git a/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.ts b/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.ts new file mode 100644 index 0000000000..734b574edd --- /dev/null +++ b/plugins/events-backend-module-gerrit/src/service/GerritEventRouterEventsModule.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { GerritEventRouter } from '../router/GerritEventRouter'; + +/** + * Module for the events-backend plugin, adding an event router for Gerrit. + * + * Registers the {@link GerritEventRouter}. + * + * @alpha + */ +export const gerritEventRouterEventsModule = createBackendModule({ + pluginId: 'events', + moduleId: 'gerritEventRouter', + register(env) { + env.registerInit({ + deps: { + events: eventsExtensionPoint, + }, + async init({ events }) { + const eventRouter = new GerritEventRouter(); + + events.addPublishers(eventRouter); + events.addSubscribers(eventRouter); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-gerrit/src/setupTests.ts b/plugins/events-backend-module-gerrit/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/events-backend-module-gerrit/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/events-backend-module-github/.eslintrc.js b/plugins/events-backend-module-github/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-backend-module-github/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md new file mode 100644 index 0000000000..98d9564bf1 --- /dev/null +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -0,0 +1,48 @@ +# @backstage/plugin-events-backend-module-github + +## 0.1.1-next.1 + +### Patch Changes + +- 0f46ec304c: Add `createGithubSignatureValidator(config)` which can be used + to create a validator used at an ingress for topic `github`. + + On top, there is a new `githubWebhookEventsModule` for the new backend plugin API + which auto-registers the `HttpPostIngress` for topic `github` incl. the validator. + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md. + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## 0.1.0 + +### Minor Changes + +- b3a4edb885: Adds a new module `github` to plugin-events-backend. + + The module adds a new event router `GithubEventRouter`. + + The event router will re-publish events received at topic `github` + under a more specific topic depending on their `x-github-event` value + (e.g., `github.push`). + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 diff --git a/plugins/events-backend-module-github/README.md b/plugins/events-backend-module-github/README.md new file mode 100644 index 0000000000..fb0a0d1c35 --- /dev/null +++ b/plugins/events-backend-module-github/README.md @@ -0,0 +1,74 @@ +# events-backend-module-github + +Welcome to the `events-backend-module-github` backend plugin! + +This plugin is a module for the `events-backend` backend plugin +and extends it with an `GithubEventRouter`. + +The event router will subscribe to the topic `github` +and route the events to more concrete topics based on the value +of the provided `x-github-event` metadata field. + +Examples: + +| `x-github-event` | topic | +| ---------------- | --------------------- | +| `pull_request` | `github.pull_request` | +| `push` | `github.push` | +| `repository` | `github.repository` | + +Please find all possible webhook event types at the +[official documentation](https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads). + +## Installation + +Install the [`events-backend` plugin](../events-backend/README.md). + +Install this module: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-events-backend-module-github +``` + +Add the event router to the `EventsBackend`: + +```diff ++const githubEventRouter = new GithubEventRouter(); + + EventsBackend ++ .addPublishers(githubEventRouter) ++ .addSubscribers(githubEventRouter); +// [...] +``` + +### Signature Validator + +Add the signature validator for the topic `github`: + +```diff +// at packages/backend/src/plugins/events.ts ++ import { createGithubSignatureValidator } from '@backstage/plugin-events-backend-module-github'; +// [...] + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + ingresses: { ++ github: { ++ validator: createGithubSignatureValidator(env.config), ++ }, + }, + logger: env.logger, + }); +``` + +Additionally, you need to add the configuration: + +```yaml +events: + modules: + github: + webhookSecret: your-secret-token +``` + +Configuration at GitHub: +https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks diff --git a/plugins/events-backend-module-github/api-report.md b/plugins/events-backend-module-github/api-report.md new file mode 100644 index 0000000000..b2a4425ae6 --- /dev/null +++ b/plugins/events-backend-module-github/api-report.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/plugin-events-backend-module-github" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; +import { EventParams } from '@backstage/plugin-events-node'; +import { RequestValidator } from '@backstage/plugin-events-node'; +import { SubTopicEventRouter } from '@backstage/plugin-events-node'; + +// @public +export function createGithubSignatureValidator( + config: Config, +): RequestValidator; + +// @public +export class GithubEventRouter extends SubTopicEventRouter { + constructor(); + // (undocumented) + protected determineSubTopic(params: EventParams): string | undefined; +} + +// @alpha +export const githubEventRouterEventsModule: ( + options?: undefined, +) => BackendFeature; + +// @alpha +export const githubWebhookEventsModule: (options?: undefined) => BackendFeature; +``` diff --git a/plugins/events-backend-module-github/config.d.ts b/plugins/events-backend-module-github/config.d.ts new file mode 100644 index 0000000000..692d8d4fb5 --- /dev/null +++ b/plugins/events-backend-module-github/config.d.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + events?: { + modules?: { + /** + * events-backend-module-github plugin configuration. + */ + github?: { + /** + * Secret token for webhook requests used to verify signatures. + * + * See https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks + * for more details. + * + * @visibility secret + */ + webhookSecret?: string; + }; + }; + }; +} diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json new file mode 100644 index 0000000000..a8b973976f --- /dev/null +++ b/plugins/events-backend-module-github/package.json @@ -0,0 +1,44 @@ +{ + "name": "@backstage/plugin-events-backend-module-github", + "version": "0.1.1-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", + "@octokit/webhooks-methods": "^3.0.0", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", + "supertest": "^6.1.3" + }, + "files": [ + "alpha", + "config.d.ts", + "dist" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.test.ts b/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.test.ts new file mode 100644 index 0000000000..32d67437a8 --- /dev/null +++ b/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.test.ts @@ -0,0 +1,102 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { + RequestDetails, + RequestRejectionDetails, + RequestValidationContext, +} from '@backstage/plugin-events-node'; +import { sign } from '@octokit/webhooks-methods'; +import { createGithubSignatureValidator } from './createGithubSignatureValidator'; + +class TestContext implements RequestValidationContext { + #details?: Partial; + + reject(details?: Partial): void { + this.#details = details; + } + + get details() { + return this.#details; + } +} + +describe('createGithubSignatureValidator', () => { + const secret = 'valid-secret'; + const configWithoutSecret = new ConfigReader({}); + const configWithSecret = new ConfigReader({ + events: { + modules: { + github: { + webhookSecret: secret, + }, + }, + }, + }); + const payload = { test: 'payload' }; + const payloadString = JSON.stringify(payload); + const validSignature = sign({ secret, algorithm: 'sha256' }, payloadString); + + const requestWithSignature = async (signature: string | undefined) => { + return { + body: payload, + headers: { + 'x-hub-signature-256': signature, + }, + } as RequestDetails; + }; + + it('no secret configured, throw error', async () => { + expect(() => createGithubSignatureValidator(configWithoutSecret)).toThrow( + "Missing required config value at 'events.modules.github.webhookSecret'", + ); + }); + + it('secret configured, reject request without signature', async () => { + const request = await requestWithSignature(undefined); + const context = new TestContext(); + + const validator = createGithubSignatureValidator(configWithSecret); + await validator(request, context); + + expect(context.details).not.toBeUndefined(); + expect(context.details?.status).toBe(403); + expect(context.details?.payload).toEqual({ message: 'invalid signature' }); + }); + + it('secret configured, reject request with invalid signature', async () => { + const request = await requestWithSignature('invalid signature'); + const context = new TestContext(); + + const validator = createGithubSignatureValidator(configWithSecret); + await validator(request, context); + + expect(context.details).not.toBeUndefined(); + expect(context.details?.status).toBe(403); + expect(context.details?.payload).toEqual({ message: 'invalid signature' }); + }); + + it('secret configured, accept request with valid signature', async () => { + const request = await requestWithSignature(await validSignature); + const context = new TestContext(); + + const validator = createGithubSignatureValidator(configWithSecret); + await validator(request, context); + + expect(context.details).toBeUndefined(); + }); +}); diff --git a/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.ts b/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.ts new file mode 100644 index 0000000000..e977640ce8 --- /dev/null +++ b/plugins/events-backend-module-github/src/http/createGithubSignatureValidator.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { + RequestDetails, + RequestValidationContext, + RequestValidator, +} from '@backstage/plugin-events-node'; +import { verify } from '@octokit/webhooks-methods'; + +/** + * Validates that the request received is the expected GitHub request + * using the signature received with the `x-hub-signature-256` header + * which is based on a secret token configured at GitHub and here. + * + * See https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks + * for more details. + * + * @param config - root config + * @public + */ +export function createGithubSignatureValidator( + config: Config, +): RequestValidator { + const secret = config.getString('events.modules.github.webhookSecret'); + + return async ( + request: RequestDetails, + context: RequestValidationContext, + ): Promise => { + const signature = request.headers['x-hub-signature-256'] as + | string + | undefined; + + if ( + !signature || + !(await verify(secret, JSON.stringify(request.body), signature)) + ) { + context.reject({ + status: 403, + payload: { message: 'invalid signature' }, + }); + } + }; +} diff --git a/plugins/events-backend-module-github/src/index.ts b/plugins/events-backend-module-github/src/index.ts new file mode 100644 index 0000000000..8e5671d9d8 --- /dev/null +++ b/plugins/events-backend-module-github/src/index.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * The module `github` for the Backstage backend plugin "events-backend" + * adding an event router and signature validator for GitHub. + * + * @packageDocumentation + */ + +export { createGithubSignatureValidator } from './http/createGithubSignatureValidator'; +export { GithubEventRouter } from './router/GithubEventRouter'; +export { githubEventRouterEventsModule } from './service/GithubEventRouterEventsModule'; +export { githubWebhookEventsModule } from './service/GithubWebhookEventsModule'; diff --git a/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts b/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts new file mode 100644 index 0000000000..14cf6b9933 --- /dev/null +++ b/plugins/events-backend-module-github/src/router/GithubEventRouter.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { GithubEventRouter } from './GithubEventRouter'; + +describe('GithubEventRouter', () => { + const eventRouter = new GithubEventRouter(); + const topic = 'github'; + const eventPayload = { test: 'payload' }; + const metadata = { 'x-github-event': 'test_type' }; + + it('no x-github-event', () => { + const eventBroker = new TestEventBroker(); + eventRouter.setEventBroker(eventBroker); + + eventRouter.onEvent({ topic, eventPayload }); + + expect(eventBroker.published).toEqual([]); + }); + + it('with x-github-event', () => { + const eventBroker = new TestEventBroker(); + eventRouter.setEventBroker(eventBroker); + + eventRouter.onEvent({ topic, eventPayload, metadata }); + + expect(eventBroker.published.length).toBe(1); + expect(eventBroker.published[0].topic).toEqual('github.test_type'); + expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); + expect(eventBroker.published[0].metadata).toEqual(metadata); + }); +}); diff --git a/plugins/events-backend-module-github/src/router/GithubEventRouter.ts b/plugins/events-backend-module-github/src/router/GithubEventRouter.ts new file mode 100644 index 0000000000..10dd1c55c6 --- /dev/null +++ b/plugins/events-backend-module-github/src/router/GithubEventRouter.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EventParams, + SubTopicEventRouter, +} from '@backstage/plugin-events-node'; + +/** + * Subscribes to the generic `github` topic + * and publishes the events under the more concrete sub-topic + * depending on the `x-github-event` provided. + * + * @public + */ +export class GithubEventRouter extends SubTopicEventRouter { + constructor() { + super('github'); + } + + protected determineSubTopic(params: EventParams): string | undefined { + return params.metadata?.['x-github-event'] as string | undefined; + } +} diff --git a/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.test.ts b/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.test.ts new file mode 100644 index 0000000000..694f5e1dbe --- /dev/null +++ b/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { startTestBackend } from '@backstage/backend-test-utils'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { githubEventRouterEventsModule } from './GithubEventRouterEventsModule'; +import { GithubEventRouter } from '../router/GithubEventRouter'; + +describe('githubEventRouterEventsModule', () => { + it('should be correctly wired and set up', async () => { + let addedPublisher: GithubEventRouter | undefined; + let addedSubscriber: GithubEventRouter | undefined; + const extensionPoint = { + addPublishers: (publisher: any) => { + addedPublisher = publisher; + }, + addSubscribers: (subscriber: any) => { + addedSubscriber = subscriber; + }, + }; + + await startTestBackend({ + extensionPoints: [[eventsExtensionPoint, extensionPoint]], + services: [], + features: [githubEventRouterEventsModule()], + }); + + expect(addedPublisher).not.toBeUndefined(); + expect(addedPublisher).toBeInstanceOf(GithubEventRouter); + expect(addedSubscriber).not.toBeUndefined(); + expect(addedSubscriber).toBeInstanceOf(GithubEventRouter); + }); +}); diff --git a/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.ts b/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.ts new file mode 100644 index 0000000000..8914d07e71 --- /dev/null +++ b/plugins/events-backend-module-github/src/service/GithubEventRouterEventsModule.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { GithubEventRouter } from '../router/GithubEventRouter'; + +/** + * Module for the events-backend plugin, adding an event router for GitHub. + * + * Registers the {@link GithubEventRouter}. + * + * @alpha + */ +export const githubEventRouterEventsModule = createBackendModule({ + pluginId: 'events', + moduleId: 'githubEventRouter', + register(env) { + env.registerInit({ + deps: { + events: eventsExtensionPoint, + }, + async init({ events }) { + const eventRouter = new GithubEventRouter(); + + events.addPublishers(eventRouter); + events.addSubscribers(eventRouter); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.test.ts b/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.test.ts new file mode 100644 index 0000000000..84233eedee --- /dev/null +++ b/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.test.ts @@ -0,0 +1,90 @@ +/* + * 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 } from '@backstage/backend-plugin-api'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { + eventsExtensionPoint, + HttpPostIngressOptions, + RequestDetails, +} from '@backstage/plugin-events-node'; +import { sign } from '@octokit/webhooks-methods'; +import { githubWebhookEventsModule } from './GithubWebhookEventsModule'; + +describe('githubWebhookEventsModule', () => { + const secret = 'valid-secret'; + const payload = { test: 'payload' }; + const payloadString = JSON.stringify(payload); + const validSignature = sign({ secret, algorithm: 'sha256' }, payloadString); + const requestWithSignature = async (signature?: string) => { + return { + body: payload, + headers: { + 'x-hub-signature-256': signature, + }, + } as RequestDetails; + }; + + it('should be correctly wired and set up', async () => { + let addedIngress: HttpPostIngressOptions | undefined; + const extensionPoint = { + addHttpPostIngress: (ingress: any) => { + addedIngress = ingress; + }, + }; + + const config = new ConfigReader({ + events: { + modules: { + github: { + webhookSecret: secret, + }, + }, + }, + }); + + await startTestBackend({ + extensionPoints: [[eventsExtensionPoint, extensionPoint]], + services: [[coreServices.config, config]], + features: [githubWebhookEventsModule()], + }); + + expect(addedIngress).not.toBeUndefined(); + expect(addedIngress?.topic).toEqual('github'); + expect(addedIngress?.validator).not.toBeUndefined(); + const rejections: any[] = []; + const context = { + reject: (details: { status?: any; payload?: any }) => { + rejections.push(details); + }, + }; + await addedIngress!.validator!(await requestWithSignature(), context); + expect(rejections).toEqual([ + { + status: 403, + payload: { + message: 'invalid signature', + }, + }, + ]); + await addedIngress!.validator!( + await requestWithSignature(await validSignature), + context, + ); + expect(rejections.length).toEqual(1); + }); +}); diff --git a/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.ts b/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.ts new file mode 100644 index 0000000000..9a3202cb97 --- /dev/null +++ b/plugins/events-backend-module-github/src/service/GithubWebhookEventsModule.ts @@ -0,0 +1,48 @@ +/* + * 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, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { createGithubSignatureValidator } from '../http/createGithubSignatureValidator'; + +/** + * Module for the events-backend plugin, + * registering an HTTP POST ingress with request validator + * which verifies the webhook signature based on a secret. + * + * @alpha + */ +export const githubWebhookEventsModule = createBackendModule({ + pluginId: 'events', + moduleId: 'githubWebhook', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + events: eventsExtensionPoint, + }, + async init({ config, events }) { + events.addHttpPostIngress({ + topic: 'github', + validator: createGithubSignatureValidator(config), + }); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-github/src/setupTests.ts b/plugins/events-backend-module-github/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/events-backend-module-github/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/events-backend-module-gitlab/.eslintrc.js b/plugins/events-backend-module-gitlab/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-backend-module-gitlab/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md new file mode 100644 index 0000000000..593fca46ae --- /dev/null +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -0,0 +1,48 @@ +# @backstage/plugin-events-backend-module-gitlab + +## 0.1.1-next.1 + +### Patch Changes + +- 31fe8f256a: Add `createGitlabTokenValidator(config)` which can be used + to create a validator used at an ingress for topic `gitlab`. + + On top, there is a new `gitlabWebhookEventsModule` for the new backend plugin API + which auto-registers the `HttpPostIngress` for topic `gitlab` incl. the validator. + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-gitlab/README.md. + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + +## 0.1.0 + +### Minor Changes + +- 63f7983398: Adds a new module `gitlab` to plugin-events-backend. + + The module adds a new event router `GitlabEventRouter`. + + The event router will re-publish events received at topic `gitlab` + under a more specific topic depending on their `$.event_name` value + (e.g., `gitlab.push`). + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-gitlab/README.md. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 diff --git a/plugins/events-backend-module-gitlab/README.md b/plugins/events-backend-module-gitlab/README.md new file mode 100644 index 0000000000..dbe9f7bfe3 --- /dev/null +++ b/plugins/events-backend-module-gitlab/README.md @@ -0,0 +1,70 @@ +# events-backend-module-gitlab + +Welcome to the `events-backend-module-gitlab` backend plugin! + +This plugin is a module for the `events-backend` backend plugin +and extends it with an `GitlabEventRouter`. + +The event router will subscribe to the topic `gitlab` +and route the events to more concrete topics based on the value +of the provided `$.event_name` payload field. + +Examples: + +| `$.event_name` | topic | +| --------------- | ---------------------- | +| `push` | `gitlab.push` | +| `merge_request` | `gitlab.merge_request` | + +Please find all possible webhook event types at the +[official documentation](https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html). + +## Installation + +Install the [`events-backend` plugin](../events-backend/README.md). + +Install this module: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-events-backend-module-gitlab +``` + +Add the event router to the `EventsBackend`: + +```diff ++const gitlabEventRouter = new GitlabEventRouter(); + + EventsBackend ++ .addPublishers(gitlabEventRouter) ++ .addSubscribers(gitlabEventRouter); +// [...] +``` + +### Token Validator + +Add the token validator for the topic `gitlab`: + +```diff +// at packages/backend/src/plugins/events.ts ++ import { createGitlabTokenValidator } from '@backstage/plugin-events-backend-module-gitlab'; +// [...] + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + ingresses: { ++ gitlab: { ++ validator: createGitlabTokenValidator(env.config), ++ }, + }, + logger: env.logger, + }); +``` + +Additionally, you need to add the configuration: + +```yaml +events: + modules: + gitlab: + webhookSecret: your-secret-token +``` diff --git a/plugins/events-backend-module-gitlab/api-report.md b/plugins/events-backend-module-gitlab/api-report.md new file mode 100644 index 0000000000..8f130902a2 --- /dev/null +++ b/plugins/events-backend-module-gitlab/api-report.md @@ -0,0 +1,29 @@ +## API Report File for "@backstage/plugin-events-backend-module-gitlab" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; +import { EventParams } from '@backstage/plugin-events-node'; +import { RequestValidator } from '@backstage/plugin-events-node'; +import { SubTopicEventRouter } from '@backstage/plugin-events-node'; + +// @public +export function createGitlabTokenValidator(config: Config): RequestValidator; + +// @public +export class GitlabEventRouter extends SubTopicEventRouter { + constructor(); + // (undocumented) + protected determineSubTopic(params: EventParams): string | undefined; +} + +// @alpha +export const gitlabEventRouterEventsModule: ( + options?: undefined, +) => BackendFeature; + +// @alpha +export const gitlabWebhookEventsModule: (options?: undefined) => BackendFeature; +``` diff --git a/plugins/events-backend-module-gitlab/config.d.ts b/plugins/events-backend-module-gitlab/config.d.ts new file mode 100644 index 0000000000..6d02533845 --- /dev/null +++ b/plugins/events-backend-module-gitlab/config.d.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + events?: { + modules?: { + /** + * events-backend-module-gitlab plugin configuration. + */ + gitlab?: { + /** + * Secret token for webhook requests used to verify tokens. + * + * See https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#validate-payloads-by-using-a-secret-token + * for more details. + * + * @visibility secret + */ + webhookSecret?: string; + }; + }; + }; +} diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json new file mode 100644 index 0000000000..4cfcf434ff --- /dev/null +++ b/plugins/events-backend-module-gitlab/package.json @@ -0,0 +1,43 @@ +{ + "name": "@backstage/plugin-events-backend-module-gitlab", + "version": "0.1.1-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", + "supertest": "^6.1.3" + }, + "files": [ + "alpha", + "config.d.ts", + "dist" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/events-backend-module-gitlab/src/http/createGitlabTokenValidator.test.ts b/plugins/events-backend-module-gitlab/src/http/createGitlabTokenValidator.test.ts new file mode 100644 index 0000000000..33e677450e --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/http/createGitlabTokenValidator.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { + RequestDetails, + RequestRejectionDetails, + RequestValidationContext, +} from '@backstage/plugin-events-node'; +import { createGitlabTokenValidator } from './createGitlabTokenValidator'; + +class TestContext implements RequestValidationContext { + #details?: Partial; + + reject(details?: Partial): void { + this.#details = details; + } + + get details() { + return this.#details; + } +} + +describe('createGitlabTokenValidator', () => { + const validToken = 'valid-token'; + const configWithoutSecret = new ConfigReader({}); + const configWithSecret = new ConfigReader({ + events: { + modules: { + gitlab: { + webhookSecret: validToken, + }, + }, + }, + }); + + const requestWithToken = (token: string | undefined) => { + return { + body: undefined, + headers: { + 'x-gitlab-token': token, + }, + } as RequestDetails; + }; + + it('no secret configured, throw error', async () => { + expect(() => createGitlabTokenValidator(configWithoutSecret)).toThrow( + "Missing required config value at 'events.modules.gitlab.webhookSecret'", + ); + }); + + it('secret configured, reject request without token', async () => { + const request = requestWithToken(undefined); + const context = new TestContext(); + + const validator = createGitlabTokenValidator(configWithSecret); + await validator(request, context); + + expect(context.details).not.toBeUndefined(); + expect(context.details?.status).toBe(403); + expect(context.details?.payload).toEqual({ message: 'invalid token' }); + }); + + it('secret configured, reject request with invalid token', async () => { + const request = requestWithToken('invalid-token'); + const context = new TestContext(); + + const validator = createGitlabTokenValidator(configWithSecret); + await validator(request, context); + + expect(context.details).not.toBeUndefined(); + expect(context.details?.status).toBe(403); + expect(context.details?.payload).toEqual({ message: 'invalid token' }); + }); + + it('secret configured, accept request with valid token', async () => { + const request = requestWithToken(validToken); + const context = new TestContext(); + + const validator = createGitlabTokenValidator(configWithSecret); + await validator(request, context); + + expect(context.details).toBeUndefined(); + }); +}); diff --git a/plugins/events-backend-module-gitlab/src/http/createGitlabTokenValidator.ts b/plugins/events-backend-module-gitlab/src/http/createGitlabTokenValidator.ts new file mode 100644 index 0000000000..9ebc054194 --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/http/createGitlabTokenValidator.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { + RequestDetails, + RequestValidationContext, + RequestValidator, +} from '@backstage/plugin-events-node'; + +/** + * Validates a configured secret token against the token received with the `x-gitlab-token` header. + * + * See https://docs.gitlab.com/ee/user/project/integrations/webhooks.html#validate-payloads-by-using-a-secret-token + * for more details. + * + * @param config - root config + * @public + */ +export function createGitlabTokenValidator(config: Config): RequestValidator { + const secret = config.getString('events.modules.gitlab.webhookSecret'); + + return async ( + request: RequestDetails, + context: RequestValidationContext, + ): Promise => { + const token = request.headers['x-gitlab-token'] as string | undefined; + + if (secret !== token) { + context.reject({ + status: 403, + payload: { message: 'invalid token' }, + }); + } + }; +} diff --git a/plugins/events-backend-module-gitlab/src/index.ts b/plugins/events-backend-module-gitlab/src/index.ts new file mode 100644 index 0000000000..859a6c69c0 --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/index.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * The module "gitlab" for the Backstage backend plugin "events-backend" + * adding an event router and token validator for GitLab. + * + * @packageDocumentation + */ + +export { createGitlabTokenValidator } from './http/createGitlabTokenValidator'; +export { GitlabEventRouter } from './router/GitlabEventRouter'; +export { gitlabEventRouterEventsModule } from './service/GitlabEventRouterEventsModule'; +export { gitlabWebhookEventsModule } from './service/GitlabWebhookEventsModule'; diff --git a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts new file mode 100644 index 0000000000..6ced12d3cb --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import { GitlabEventRouter } from './GitlabEventRouter'; + +describe('GitlabEventRouter', () => { + const eventRouter = new GitlabEventRouter(); + const topic = 'gitlab'; + const eventPayload = { event_name: 'test_type', test: 'payload' }; + const metadata = {}; + + it('no $.event_name', () => { + const eventBroker = new TestEventBroker(); + eventRouter.setEventBroker(eventBroker); + + eventRouter.onEvent({ + topic, + eventPayload: { invalid: 'payload' }, + metadata, + }); + + expect(eventBroker.published).toEqual([]); + }); + + it('with $.event_name', () => { + const eventBroker = new TestEventBroker(); + eventRouter.setEventBroker(eventBroker); + + eventRouter.onEvent({ topic, eventPayload, metadata }); + + expect(eventBroker.published.length).toBe(1); + expect(eventBroker.published[0].topic).toEqual('gitlab.test_type'); + expect(eventBroker.published[0].eventPayload).toEqual(eventPayload); + expect(eventBroker.published[0].metadata).toEqual(metadata); + }); +}); diff --git a/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts new file mode 100644 index 0000000000..16324340ee --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/router/GitlabEventRouter.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EventParams, + SubTopicEventRouter, +} from '@backstage/plugin-events-node'; + +/** + * Subscribes to the generic `gitlab` topic + * and publishes the events under the more concrete sub-topic + * depending on the `$.event_name` field provided. + * + * @public + */ +export class GitlabEventRouter extends SubTopicEventRouter { + constructor() { + super('gitlab'); + } + + protected determineSubTopic(params: EventParams): string | undefined { + if ('event_name' in (params.eventPayload as object)) { + const payload = params.eventPayload as { event_name: string }; + return payload.event_name; + } + + return undefined; + } +} diff --git a/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.test.ts b/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.test.ts new file mode 100644 index 0000000000..f0ffca3397 --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { startTestBackend } from '@backstage/backend-test-utils'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { gitlabEventRouterEventsModule } from './GitlabEventRouterEventsModule'; +import { GitlabEventRouter } from '../router/GitlabEventRouter'; + +describe('gitlabEventRouterEventsModule', () => { + it('should be correctly wired and set up', async () => { + let addedPublisher: GitlabEventRouter | undefined; + let addedSubscriber: GitlabEventRouter | undefined; + const extensionPoint = { + addPublishers: (publisher: any) => { + addedPublisher = publisher; + }, + addSubscribers: (subscriber: any) => { + addedSubscriber = subscriber; + }, + }; + + await startTestBackend({ + extensionPoints: [[eventsExtensionPoint, extensionPoint]], + services: [], + features: [gitlabEventRouterEventsModule()], + }); + + expect(addedPublisher).not.toBeUndefined(); + expect(addedPublisher).toBeInstanceOf(GitlabEventRouter); + expect(addedSubscriber).not.toBeUndefined(); + expect(addedSubscriber).toBeInstanceOf(GitlabEventRouter); + }); +}); diff --git a/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.ts b/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.ts new file mode 100644 index 0000000000..9ab2e1263e --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/service/GitlabEventRouterEventsModule.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { GitlabEventRouter } from '../router/GitlabEventRouter'; + +/** + * Module for the events-backend plugin, adding an event router for GitLab. + * + * Registers the {@link GitlabEventRouter}. + * + * @alpha + */ +export const gitlabEventRouterEventsModule = createBackendModule({ + pluginId: 'events', + moduleId: 'gitlabEventRouter', + register(env) { + env.registerInit({ + deps: { + events: eventsExtensionPoint, + }, + async init({ events }) { + const eventRouter = new GitlabEventRouter(); + + events.addPublishers(eventRouter); + events.addSubscribers(eventRouter); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.test.ts b/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.test.ts new file mode 100644 index 0000000000..b139f46f55 --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.test.ts @@ -0,0 +1,82 @@ +/* + * 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 } from '@backstage/backend-plugin-api'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { + eventsExtensionPoint, + HttpPostIngressOptions, + RequestDetails, +} from '@backstage/plugin-events-node'; +import { gitlabWebhookEventsModule } from './GitlabWebhookEventsModule'; + +describe('gitlabWebhookEventsModule', () => { + const requestWithToken = (token?: string) => { + return { + body: undefined, + headers: { + 'x-gitlab-token': token, + }, + } as RequestDetails; + }; + + it('should be correctly wired and set up', async () => { + let addedIngress: HttpPostIngressOptions | undefined; + const extensionPoint = { + addHttpPostIngress: (ingress: any) => { + addedIngress = ingress; + }, + }; + + const config = new ConfigReader({ + events: { + modules: { + gitlab: { + webhookSecret: 'test-secret', + }, + }, + }, + }); + + await startTestBackend({ + extensionPoints: [[eventsExtensionPoint, extensionPoint]], + services: [[coreServices.config, config]], + features: [gitlabWebhookEventsModule()], + }); + + expect(addedIngress).not.toBeUndefined(); + expect(addedIngress?.topic).toEqual('gitlab'); + expect(addedIngress?.validator).not.toBeUndefined(); + const rejections: any[] = []; + const context = { + reject: (details: { status?: any; payload?: any }) => { + rejections.push(details); + }, + }; + await addedIngress!.validator!(requestWithToken(), context); + expect(rejections).toEqual([ + { + status: 403, + payload: { + message: 'invalid token', + }, + }, + ]); + await addedIngress!.validator!(requestWithToken('test-secret'), context); + expect(rejections.length).toEqual(1); + }); +}); diff --git a/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.ts b/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.ts new file mode 100644 index 0000000000..44f13f31ef --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/service/GitlabWebhookEventsModule.ts @@ -0,0 +1,50 @@ +/* + * 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, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { createGitlabTokenValidator } from '../http/createGitlabTokenValidator'; + +/** + * Module for the events-backend plugin, + * registering an HTTP POST ingress with request validator + * which verifies the webhook token based on a secret. + * + * Registers the {@link GitlabEventRouter}. + * + * @alpha + */ +export const gitlabWebhookEventsModule = createBackendModule({ + pluginId: 'events', + moduleId: 'gitlabWebhook', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + events: eventsExtensionPoint, + }, + async init({ config, events }) { + events.addHttpPostIngress({ + topic: 'gitlab', + validator: createGitlabTokenValidator(config), + }); + }, + }); + }, +}); diff --git a/plugins/events-backend-module-gitlab/src/setupTests.ts b/plugins/events-backend-module-gitlab/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/events-backend-module-gitlab/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/events-backend-test-utils/.eslintrc.js b/plugins/events-backend-test-utils/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-backend-test-utils/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md new file mode 100644 index 0000000000..fdab7ef51c --- /dev/null +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -0,0 +1,33 @@ +# @backstage/plugin-events-backend-test-utils + +## 0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.1 + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + +## 0.1.0 + +### Minor Changes + +- 7bbd2403a1: Adds a new backend plugin plugin-events-backend for managing events. + + plugin-events-node exposes interfaces which can be used by modules. + + plugin-events-backend-test-utils provides utilities which can be used while writing tests e.g. for modules. + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.1.0 diff --git a/plugins/events-backend-test-utils/README.md b/plugins/events-backend-test-utils/README.md new file mode 100644 index 0000000000..c8727b536b --- /dev/null +++ b/plugins/events-backend-test-utils/README.md @@ -0,0 +1,4 @@ +# plugin-events-backend-test-utils + +Houses implementations of plugin-events-node interfaces +which can be useful for test for events-backend and its modules. diff --git a/plugins/events-backend-test-utils/api-report.md b/plugins/events-backend-test-utils/api-report.md new file mode 100644 index 0000000000..46131c4244 --- /dev/null +++ b/plugins/events-backend-test-utils/api-report.md @@ -0,0 +1,47 @@ +## API Report File for "@backstage/plugin-events-backend-test-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { EventBroker } from '@backstage/plugin-events-node'; +import { EventParams } from '@backstage/plugin-events-node'; +import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventSubscriber } from '@backstage/plugin-events-node'; + +// @public (undocumented) +export class TestEventBroker implements EventBroker { + // (undocumented) + publish(params: EventParams): Promise; + // (undocumented) + readonly published: EventParams[]; + // (undocumented) + subscribe( + ...subscribers: Array> + ): void; + // (undocumented) + readonly subscribed: EventSubscriber[]; +} + +// @public (undocumented) +export class TestEventPublisher implements EventPublisher { + // (undocumented) + get eventBroker(): EventBroker | undefined; + // (undocumented) + setEventBroker(eventBroker: EventBroker): Promise; +} + +// @public (undocumented) +export class TestEventSubscriber implements EventSubscriber { + constructor(name: string, topics: string[]); + // (undocumented) + readonly name: string; + // (undocumented) + onEvent(params: EventParams): Promise; + // (undocumented) + readonly receivedEvents: Record; + // (undocumented) + supportsEventTopics(): string[]; + // (undocumented) + readonly topics: string[]; +} +``` diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json new file mode 100644 index 0000000000..8cfb19c761 --- /dev/null +++ b/plugins/events-backend-test-utils/package.json @@ -0,0 +1,34 @@ +{ + "name": "@backstage/plugin-events-backend-test-utils", + "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", + "version": "0.1.1-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/plugin-events-node": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/events-backend-test-utils/src/index.ts b/plugins/events-backend-test-utils/src/index.ts new file mode 100644 index 0000000000..efba5be0d0 --- /dev/null +++ b/plugins/events-backend-test-utils/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The events-test-utils module for `@backstage/plugin-events-node`. + * + * @packageDocumentation + */ + +export * from './testUtils'; diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts new file mode 100644 index 0000000000..c697a6506f --- /dev/null +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventBroker.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EventBroker, + EventParams, + EventSubscriber, +} from '@backstage/plugin-events-node'; + +/** @public */ +export class TestEventBroker implements EventBroker { + readonly published: EventParams[] = []; + readonly subscribed: EventSubscriber[] = []; + + async publish(params: EventParams): Promise { + this.published.push(params); + } + + subscribe( + ...subscribers: Array> + ): void { + this.subscribed.push(...subscribers.flat()); + } +} diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts new file mode 100644 index 0000000000..c1b2038afb --- /dev/null +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventPublisher.ts @@ -0,0 +1,30 @@ +/* + * 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 { EventBroker, EventPublisher } from '@backstage/plugin-events-node'; + +/** @public */ +export class TestEventPublisher implements EventPublisher { + #eventBroker?: EventBroker; + + async setEventBroker(eventBroker: EventBroker): Promise { + this.#eventBroker = eventBroker; + } + + get eventBroker() { + return this.#eventBroker; + } +} diff --git a/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts b/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts new file mode 100644 index 0000000000..ef5758b804 --- /dev/null +++ b/plugins/events-backend-test-utils/src/testUtils/TestEventSubscriber.ts @@ -0,0 +1,39 @@ +/* + * 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 { EventParams, EventSubscriber } from '@backstage/plugin-events-node'; + +/** @public */ +export class TestEventSubscriber implements EventSubscriber { + readonly name: string; + readonly topics: string[]; + + readonly receivedEvents: Record = {}; + + constructor(name: string, topics: string[]) { + this.name = name; + this.topics = topics; + } + + supportsEventTopics(): string[] { + return this.topics; + } + + async onEvent(params: EventParams): Promise { + this.receivedEvents[params.topic] = this.receivedEvents[params.topic] ?? []; + this.receivedEvents[params.topic].push(params); + } +} diff --git a/plugins/events-backend-test-utils/src/testUtils/index.ts b/plugins/events-backend-test-utils/src/testUtils/index.ts new file mode 100644 index 0000000000..a571ba3075 --- /dev/null +++ b/plugins/events-backend-test-utils/src/testUtils/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { TestEventBroker } from './TestEventBroker'; +export { TestEventPublisher } from './TestEventPublisher'; +export { TestEventSubscriber } from './TestEventSubscriber'; diff --git a/plugins/events-backend/.eslintrc.js b/plugins/events-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md new file mode 100644 index 0000000000..b3efbe8a44 --- /dev/null +++ b/plugins/events-backend/CHANGELOG.md @@ -0,0 +1,80 @@ +# @backstage/plugin-events-backend + +## 0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-events-node@0.2.0-next.1 + +## 0.2.0-next.0 + +### Minor Changes + +- cf41eedf43: **BREAKING:** Remove required field `router` at `HttpPostIngressEventPublisher.fromConfig` + and replace it with `bind(router: Router)`. + Additionally, the path prefix `/http` will be added inside `HttpPostIngressEventPublisher`. + + ```diff + // at packages/backend/src/plugins/events.ts + const eventsRouter = Router(); + - const httpRouter = Router(); + - eventsRouter.use('/http', httpRouter); + + const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + logger: env.logger, + - router: httpRouter, + }); + + http.bind(eventsRouter); + ``` + +### Patch Changes + +- cf41eedf43: Introduce a new interface `RequestDetails` to abstract `Request` + providing access to request body and headers. + + **BREAKING:** Replace `request: Request` with `request: RequestDetails` at `RequestValidator`. + +- Updated dependencies + - @backstage/plugin-events-node@0.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.1.0 + +### Minor Changes + +- dc9da28abd: Support events received via HTTP endpoints at plugin-events-backend. + + The plugin provides an event publisher `HttpPostIngressEventPublisher` + which will allow you to receive events via + HTTP endpoints `POST /api/events/http/{topic}` + and will publish these to the used event broker. + + Using a provided custom validator, you can participate in the decision + which events are accepted, e.g. by verifying the source of the request. + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md. + +- 7bbd2403a1: Adds a new backend plugin plugin-events-backend for managing events. + + plugin-events-node exposes interfaces which can be used by modules. + + plugin-events-backend-test-utils provides utilities which can be used while writing tests e.g. for modules. + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-events-node@0.1.0 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/config@1.0.4 diff --git a/plugins/events-backend/README.md b/plugins/events-backend/README.md new file mode 100644 index 0000000000..cef8ac7330 --- /dev/null +++ b/plugins/events-backend/README.md @@ -0,0 +1,210 @@ +# events-backend + +Welcome to the events-backend backend plugin! + +This plugin provides the wiring of all extension points +for managing events as defined by [plugin-events-node](../events-node) +including backend plugin `EventsPlugin` and `EventsBackend`. + +Additionally, it uses a simple in-memory implementation for +the `EventBroker` by default which you can replace with a more sophisticated +implementation of your choice as you need (e.g., via module). + +Some of these (non-exhaustive) may provide added persistence, +or use external systems like AWS EventBridge, AWS SNS, Kafka, etc. + +By default, the plugin ships with support to receive events via HTTP endpoints +`POST /api/events/http/{topic}` and will publish these +to the used event broker. + +## Installation + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-events-backend +``` + +Add a file [`packages/backend/src/plugins/events.ts`](../../packages/backend/src/plugins/events.ts) +to your Backstage project. + +There, you can add all publishers, subscribers, etc. you want. + +Additionally, add the events plugin to your backend. + +```diff +// packages/backend/src/index.ts +// [...] ++import events from './plugins/events'; +// [...] ++ const eventsEnv = useHotMemoize(module, () => createEnv('events')); +// [...] ++ apiRouter.use('/events', await events(eventsEnv, [])); +// [...] +``` + +### With Event-based Entity Providers + +In case you use event-based `EntityProviders`, +you may need something like the following: + +```diff +// packages/backend/src/index.ts +- apiRouter.use('/events', await events(eventsEnv, [])); ++ apiRouter.use('/events', await events(eventsEnv, eventBasedEntityProviders)); +``` + +as well as a file +[`packages/backend/src/plugins/catalogEventBasedProviders.ts`](../../packages/backend/src/plugins/catalogEventBasedProviders.ts) +which contains event-based entity providers. + +In case you don't have this dependency added yet: + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-events-backend +``` + +```diff +// packages/backend/src/plugins/catalog.ts + import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; ++import { EntityProvider } from '@backstage/plugin-catalog-node'; + import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, ++ providers?: Array, + ): Promise { + const builder = await CatalogBuilder.create(env); + builder.addProcessor(new ScaffolderEntitiesProcessor()); ++ builder.addEntityProvider(providers ?? []); + const { processingEngine, router } = await builder.build(); + await processingEngine.start(); + return router; + } +``` + +## Configuration + +In order to create HTTP endpoints to receive events for a certain +topic, you need to add them at your configuration: + +```yaml +events: + http: + topics: + - bitbucketCloud + - github + - whatever +``` + +Only those topics added to the configuration will result in +available endpoints. + +The example above would result in the following endpoints: + +``` +POST /api/events/http/bitbucketCloud +POST /api/events/http/github +POST /api/events/http/whatever +``` + +You may want to use these for webhooks by SCM providers +in combination with suitable event subscribers. + +However, it is not limited to these use cases. + +## Use Cases + +### Custom Event Broker + +Example using the `EventsBackend`: + +```ts +new EventsBackend(env.logger) + .setEventBroker(yourEventBroker) + // [...] + .start(); +``` + +Example using a module: + +```ts +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; + +// [...] + +export const yourModuleEventsModule = createBackendModule({ + pluginId: 'events', + moduleId: 'yourModule', + register(env) { + // [...] + env.registerInit({ + deps: { + // [...] + events: eventsExtensionPoint, + // [...] + }, + async init({ /* ... */ events /*, ... */ }) { + // [...] + const yourEventBroker = new YourEventBroker(); + // [...] + events.setEventBroker(yourEventBroker); + }, + }); + }, +}); +``` + +### Request Validator + +Example using the `EventsBackend`: + +```ts +const http = HttpPostIngressEventPublisher.fromConfig({ + config: env.config, + ingresses: { + yourTopic: { + validator: yourValidator, + }, + }, + logger: env.logger, +}); +http.bind(router); + +await new EventsBackend(env.logger) + .addPublishers(http) + // [...] + .start(); +``` + +Example using a module: + +```ts +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; + +// [...] + +export const yourModuleEventsModule = createBackendModule({ + pluginId: 'events', + moduleId: 'yourModule', + register(env) { + // [...] + env.registerInit({ + deps: { + // [...] + events: eventsExtensionPoint, + // [...] + }, + async init({ /* ... */ events /*, ... */ }) { + // [...] + events.addHttpPostIngress({ + topic: 'your-topic', + validator: yourValidator, + }); + }, + }); + }, +}); +``` diff --git a/plugins/events-backend/api-report.md b/plugins/events-backend/api-report.md new file mode 100644 index 0000000000..69d7600ed3 --- /dev/null +++ b/plugins/events-backend/api-report.md @@ -0,0 +1,49 @@ +## API Report File for "@backstage/plugin-events-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { Config } from '@backstage/config'; +import { EventBroker } from '@backstage/plugin-events-node'; +import { EventPublisher } from '@backstage/plugin-events-node'; +import { EventSubscriber } from '@backstage/plugin-events-node'; +import express from 'express'; +import { HttpPostIngressOptions } from '@backstage/plugin-events-node'; +import { Logger } from 'winston'; + +// @public +export class EventsBackend { + constructor(logger: Logger); + // (undocumented) + addPublishers( + ...publishers: Array> + ): EventsBackend; + // (undocumented) + addSubscribers( + ...subscribers: Array> + ): EventsBackend; + // (undocumented) + setEventBroker(eventBroker: EventBroker): EventsBackend; + start(): Promise; +} + +// @alpha +export const eventsPlugin: (options?: undefined) => BackendFeature; + +// @public +export class HttpPostIngressEventPublisher implements EventPublisher { + // (undocumented) + bind(router: express.Router): void; + // (undocumented) + static fromConfig(env: { + config: Config; + ingresses?: { + [topic: string]: Omit; + }; + logger: Logger; + }): HttpPostIngressEventPublisher; + // (undocumented) + setEventBroker(eventBroker: EventBroker): Promise; +} +``` diff --git a/plugins/events-backend/config.d.ts b/plugins/events-backend/config.d.ts new file mode 100644 index 0000000000..3d26661855 --- /dev/null +++ b/plugins/events-backend/config.d.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + events?: { + http?: { + /** + * Topics for which a route has to be registered + * at which we can receive events via HTTP POST requests + * (i.e. received from webhooks). + */ + topics?: string[]; + }; + }; +} diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json new file mode 100644 index 0000000000..bb872b6120 --- /dev/null +++ b/plugins/events-backend/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/plugin-events-backend", + "version": "0.2.0-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-events-node": "workspace:^", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^", + "supertest": "^6.1.3" + }, + "files": [ + "alpha", + "config.d.ts", + "dist" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/events-backend/src/index.ts b/plugins/events-backend/src/index.ts new file mode 100644 index 0000000000..5026729015 --- /dev/null +++ b/plugins/events-backend/src/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The Backstage backend plugin "events" that provides the event management. + * + * @packageDocumentation + */ + +export { EventsBackend } from './service/EventsBackend'; +export { eventsPlugin } from './service/EventsPlugin'; +export { HttpPostIngressEventPublisher } from './service/http'; diff --git a/plugins/events-backend/src/service/EventsBackend.test.ts b/plugins/events-backend/src/service/EventsBackend.test.ts new file mode 100644 index 0000000000..c2041b57ac --- /dev/null +++ b/plugins/events-backend/src/service/EventsBackend.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { + TestEventBroker, + TestEventPublisher, + TestEventSubscriber, +} from '@backstage/plugin-events-backend-test-utils'; +import { EventsBackend } from './EventsBackend'; + +const logger = getVoidLogger(); + +describe('EventsBackend', () => { + it('wires up all components', async () => { + const eventBroker = new TestEventBroker(); + const publisher1 = new TestEventPublisher(); + const publisher2 = new TestEventPublisher(); + + await new EventsBackend(logger) + .setEventBroker(eventBroker) + .addPublishers(publisher1, [publisher2]) + .addSubscribers(new TestEventSubscriber('one', ['topicA']), [ + new TestEventSubscriber('two', ['topicA', 'topicB']), + ]) + .start(); + + await eventBroker.publish({ + topic: 'topicA', + eventPayload: { test: 'payload' }, + }); + + expect(eventBroker.published.length).toEqual(1); + expect(eventBroker.published[0].topic).toEqual('topicA'); + expect(eventBroker.published[0].eventPayload).toEqual({ test: 'payload' }); + + expect(eventBroker.subscribed.length).toEqual(2); + expect( + eventBroker.subscribed.map( + sub => (sub as unknown as TestEventSubscriber).name, + ), + ).toEqual(['one', 'two']); + + expect(publisher1.eventBroker).toBe(eventBroker); + expect(publisher2.eventBroker).toBe(eventBroker); + }); +}); diff --git a/plugins/events-backend/src/service/EventsBackend.ts b/plugins/events-backend/src/service/EventsBackend.ts new file mode 100644 index 0000000000..77b1b538f7 --- /dev/null +++ b/plugins/events-backend/src/service/EventsBackend.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + EventBroker, + EventPublisher, + EventSubscriber, +} from '@backstage/plugin-events-node'; +import { Logger } from 'winston'; +import { InMemoryEventBroker } from './InMemoryEventBroker'; + +/** + * A builder that helps wire up all component parts of the event management. + * + * @public + */ +export class EventsBackend { + private eventBroker: EventBroker; + private publishers: EventPublisher[] = []; + private subscribers: EventSubscriber[] = []; + + constructor(logger: Logger) { + this.eventBroker = new InMemoryEventBroker(logger); + } + + setEventBroker(eventBroker: EventBroker): EventsBackend { + this.eventBroker = eventBroker; + return this; + } + + addPublishers( + ...publishers: Array> + ): EventsBackend { + this.publishers.push(...publishers.flat()); + return this; + } + + addSubscribers( + ...subscribers: Array> + ): EventsBackend { + this.subscribers.push(...subscribers.flat()); + return this; + } + + /** + * Wires up and returns all component parts of the event management. + */ + async start(): Promise { + this.eventBroker.subscribe(this.subscribers); + this.publishers.forEach(publisher => + publisher.setEventBroker(this.eventBroker), + ); + } +} diff --git a/plugins/events-backend/src/service/EventsPlugin.test.ts b/plugins/events-backend/src/service/EventsPlugin.test.ts new file mode 100644 index 0000000000..b08364600f --- /dev/null +++ b/plugins/events-backend/src/service/EventsPlugin.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { errorHandler, getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { startTestBackend } from '@backstage/backend-test-utils'; +import { eventsExtensionPoint } from '@backstage/plugin-events-node'; +import { + TestEventBroker, + TestEventPublisher, + TestEventSubscriber, +} from '@backstage/plugin-events-backend-test-utils'; +import express from 'express'; +import Router from 'express-promise-router'; +import request from 'supertest'; +import { eventsPlugin } from './EventsPlugin'; + +describe('eventPlugin', () => { + it('should be initialized properly', async () => { + const eventBroker = new TestEventBroker(); + const publisher = new TestEventPublisher(); + const subscriber = new TestEventSubscriber('sub', ['fake']); + + const config = new ConfigReader({ + events: { + http: { + topics: ['fake'], + }, + }, + }); + + const httpRouter = Router(); + httpRouter.use(express.json()); + httpRouter.use(errorHandler()); + const app = express().use(httpRouter); + + const testModule = createBackendModule({ + pluginId: 'events', + moduleId: 'test', + register(env) { + env.registerInit({ + deps: { + events: eventsExtensionPoint, + }, + async init({ events }) { + events.setEventBroker(eventBroker); + events.addPublishers(publisher); + events.addSubscribers(subscriber); + }, + }); + }, + }); + + await startTestBackend({ + extensionPoints: [], + services: [ + [coreServices.config, config], + [coreServices.httpRouter, httpRouter], + [coreServices.logger, getVoidLogger()], + ], + features: [eventsPlugin(), testModule()], + }); + + expect(publisher.eventBroker).toBe(eventBroker); + expect(eventBroker.subscribed.length).toEqual(1); + expect(eventBroker.subscribed[0]).toBe(subscriber); + + const response = await request(app) + .post('/http/fake') + .timeout(100) + .send({ test: 'fake' }); + expect(response.status).toBe(202); + + expect(eventBroker.published.length).toEqual(1); + expect(eventBroker.published[0].topic).toEqual('fake'); + expect(eventBroker.published[0].eventPayload).toEqual({ test: 'fake' }); + }); +}); diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts new file mode 100644 index 0000000000..63d0d0b02a --- /dev/null +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -0,0 +1,123 @@ +/* + * 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 { + createBackendPlugin, + coreServices, + loggerToWinstonLogger, +} from '@backstage/backend-plugin-api'; +import { + EventBroker, + EventPublisher, + EventSubscriber, + eventsExtensionPoint, + EventsExtensionPoint, + HttpPostIngressOptions, +} from '@backstage/plugin-events-node'; +import { InMemoryEventBroker } from './InMemoryEventBroker'; +import Router from 'express-promise-router'; +import { HttpPostIngressEventPublisher } from './http'; + +class EventsExtensionPointImpl implements EventsExtensionPoint { + #eventBroker: EventBroker | undefined; + #httpPostIngresses: HttpPostIngressOptions[] = []; + #publishers: EventPublisher[] = []; + #subscribers: EventSubscriber[] = []; + + setEventBroker(eventBroker: EventBroker): void { + this.#eventBroker = eventBroker; + } + + addPublishers( + ...publishers: Array> + ): void { + this.#publishers.push(...publishers.flat()); + } + + addSubscribers( + ...subscribers: Array> + ): void { + this.#subscribers.push(...subscribers.flat()); + } + + addHttpPostIngress(options: HttpPostIngressOptions) { + this.#httpPostIngresses.push(options); + } + + get eventBroker() { + return this.#eventBroker; + } + + get publishers() { + return this.#publishers; + } + + get subscribers() { + return this.#subscribers; + } + + get httpPostIngresses() { + return this.#httpPostIngresses; + } +} + +/** + * Events plugin + * + * @alpha + */ +export const eventsPlugin = createBackendPlugin({ + id: 'events', + register(env) { + const extensionPoint = new EventsExtensionPointImpl(); + env.registerExtensionPoint(eventsExtensionPoint, extensionPoint); + + env.registerInit({ + deps: { + config: coreServices.config, + logger: coreServices.logger, + router: coreServices.httpRouter, + }, + async init({ config, logger, router }) { + const winstonLogger = loggerToWinstonLogger(logger); + + const ingresses = Object.fromEntries( + extensionPoint.httpPostIngresses.map(ingress => [ + ingress.topic, + ingress as Omit, + ]), + ); + + const http = HttpPostIngressEventPublisher.fromConfig({ + config, + ingresses, + logger: winstonLogger, + }); + const eventsRouter = Router(); + http.bind(eventsRouter); + router.use(eventsRouter); + + const eventBroker = + extensionPoint.eventBroker ?? new InMemoryEventBroker(winstonLogger); + + eventBroker.subscribe(extensionPoint.subscribers); + [extensionPoint.publishers, http] + .flat() + .forEach(publisher => publisher.setEventBroker(eventBroker)); + }, + }); + }, +}); diff --git a/plugins/events-backend/src/service/InMemoryEventBroker.test.ts b/plugins/events-backend/src/service/InMemoryEventBroker.test.ts new file mode 100644 index 0000000000..68a6f63a72 --- /dev/null +++ b/plugins/events-backend/src/service/InMemoryEventBroker.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { TestEventSubscriber } from '@backstage/plugin-events-backend-test-utils'; +import { InMemoryEventBroker } from './InMemoryEventBroker'; + +const logger = getVoidLogger(); + +describe('InMemoryEventBroker', () => { + it('passes events to interested subscribers', () => { + const subscriber1 = new TestEventSubscriber('test1', ['topicA', 'topicB']); + const subscriber2 = new TestEventSubscriber('test2', ['topicB', 'topicC']); + const eventBroker = new InMemoryEventBroker(logger); + + eventBroker.subscribe(subscriber1); + eventBroker.subscribe(subscriber2); + eventBroker.publish({ topic: 'topicA', eventPayload: { test: 'topicA' } }); + eventBroker.publish({ topic: 'topicB', eventPayload: { test: 'topicB' } }); + eventBroker.publish({ topic: 'topicC', eventPayload: { test: 'topicC' } }); + eventBroker.publish({ topic: 'topicD', eventPayload: { test: 'topicD' } }); + + expect(Object.keys(subscriber1.receivedEvents)).toEqual([ + 'topicA', + 'topicB', + ]); + expect(subscriber1.receivedEvents.topicA.length).toEqual(1); + expect(subscriber1.receivedEvents.topicA[0]).toEqual({ + topic: 'topicA', + eventPayload: { test: 'topicA' }, + }); + expect(subscriber1.receivedEvents.topicB.length).toEqual(1); + expect(subscriber1.receivedEvents.topicB[0]).toEqual({ + topic: 'topicB', + eventPayload: { test: 'topicB' }, + }); + + expect(Object.keys(subscriber2.receivedEvents)).toEqual([ + 'topicB', + 'topicC', + ]); + expect(subscriber2.receivedEvents.topicB.length).toEqual(1); + expect(subscriber2.receivedEvents.topicB[0]).toEqual({ + topic: 'topicB', + eventPayload: { test: 'topicB' }, + }); + expect(subscriber2.receivedEvents.topicC.length).toEqual(1); + expect(subscriber2.receivedEvents.topicC[0]).toEqual({ + topic: 'topicC', + eventPayload: { test: 'topicC' }, + }); + }); +}); diff --git a/plugins/events-backend/src/service/InMemoryEventBroker.ts b/plugins/events-backend/src/service/InMemoryEventBroker.ts new file mode 100644 index 0000000000..90c7d912fe --- /dev/null +++ b/plugins/events-backend/src/service/InMemoryEventBroker.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 { + EventBroker, + EventParams, + EventSubscriber, +} from '@backstage/plugin-events-node'; +import { Logger } from 'winston'; + +/** + * In-memory event broker which will pass the event to all registered subscribers + * interested in it. + * Events will not be persisted in any form. + */ +// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) +export class InMemoryEventBroker implements EventBroker { + constructor(private readonly logger: Logger) {} + + private readonly subscribers: { + [topic: string]: EventSubscriber[]; + } = {}; + + async publish(params: EventParams): Promise { + this.logger.debug( + `Event received: topic=${params.topic}, metadata=${JSON.stringify( + params.metadata, + )}, payload=${JSON.stringify(params.eventPayload)}`, + ); + + const subscribed = this.subscribers[params.topic] ?? []; + subscribed.forEach(subscriber => subscriber.onEvent(params)); + } + + subscribe( + ...subscribers: Array> + ): void { + subscribers.flat().forEach(subscriber => { + subscriber.supportsEventTopics().forEach(topic => { + this.subscribers[topic] = this.subscribers[topic] ?? []; + this.subscribers[topic].push(subscriber); + }); + }); + } +} diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts new file mode 100644 index 0000000000..72e0b94c57 --- /dev/null +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.test.ts @@ -0,0 +1,216 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; +import express from 'express'; +import Router from 'express-promise-router'; +import request from 'supertest'; +import { HttpPostIngressEventPublisher } from './HttpPostIngressEventPublisher'; + +describe('HttpPostIngressEventPublisher', () => { + const logger = getVoidLogger(); + + it('should set up routes correctly', async () => { + const config = new ConfigReader({ + events: { + http: { + topics: ['testA'], + }, + }, + }); + + const router = Router(); + const app = express().use(router); + + const publisher = HttpPostIngressEventPublisher.fromConfig({ + config, + ingresses: { + testB: {}, + }, + logger, + }); + publisher.bind(router); + + const eventBroker = new TestEventBroker(); + await publisher.setEventBroker(eventBroker); + + const notFoundResponse = await request(app) + .post('/http/unknown') + .timeout(1000) + .send({ test: 'data' }); + expect(notFoundResponse.status).toBe(404); + + const response1 = await request(app) + .post('/http/testA') + .set('X-Custom-Header', 'test-value') + .timeout(1000) + .send({ testA: 'data' }); + expect(response1.status).toBe(202); + + const response2 = await request(app) + .post('/http/testB') + .set('X-Custom-Header', 'test-value') + .timeout(1000) + .send({ testB: 'data' }); + expect(response2.status).toBe(202); + + expect(eventBroker.published.length).toEqual(2); + expect(eventBroker.published[0].topic).toEqual('testA'); + expect(eventBroker.published[0].eventPayload).toEqual({ testA: 'data' }); + expect(eventBroker.published[0].metadata).toEqual( + expect.objectContaining({ + 'content-type': 'application/json', + 'x-custom-header': 'test-value', + }), + ); + expect(eventBroker.published[1].topic).toEqual('testB'); + expect(eventBroker.published[1].eventPayload).toEqual({ testB: 'data' }); + expect(eventBroker.published[1].metadata).toEqual( + expect.objectContaining({ + 'content-type': 'application/json', + 'x-custom-header': 'test-value', + }), + ); + }); + + it('with validator', async () => { + const config = new ConfigReader({ + events: { + http: { + topics: ['testA'], + }, + }, + }); + + const router = Router(); + const app = express().use(router); + + const publisher = HttpPostIngressEventPublisher.fromConfig({ + config, + ingresses: { + testB: { + validator: async (req, context) => { + if (req.headers['x-test-signature'] === 'testB-signature') { + return; + } + + context.reject({ + status: 400, + payload: { + message: 'wrong signature', + }, + }); + }, + }, + testC: { + validator: async (req, context) => { + if (req.headers['x-test-signature'] === 'testC-signature') { + return; + } + + context.reject({ + status: 404, + // payload: {}, + }); + }, + }, + testD: { + validator: async (req, context) => { + if (req.headers['x-test-signature'] === 'testD-signature') { + return; + } + + context.reject({ + // status: 403, + // payload: {}, + }); + }, + }, + }, + logger, + }); + publisher.bind(router); + + const eventBroker = new TestEventBroker(); + await publisher.setEventBroker(eventBroker); + + const response1 = await request(app) + .post('/http/testA') + .timeout(1000) + .send({ test: 'data' }); + expect(response1.status).toBe(202); + + const response2 = await request(app) + .post('/http/testB') + .timeout(1000) + .send({ test: 'data' }); + expect(response2.status).toBe(400); + expect(response2.body).toEqual({ message: 'wrong signature' }); + + const response3 = await request(app) + .post('/http/testB') + .set('X-Test-Signature', 'wrong') + .timeout(1000) + .send({ test: 'data' }); + expect(response3.status).toBe(400); + expect(response3.body).toEqual({ message: 'wrong signature' }); + + const response4 = await request(app) + .post('/http/testB') + .set('X-Test-Signature', 'testB-signature') + .timeout(1000) + .send({ test: 'data' }); + expect(response4.status).toBe(202); + + const response5 = await request(app) + .post('/http/testC') + .timeout(1000) + .send({ test: 'data' }); + expect(response5.status).toBe(404); + expect(response5.body).toEqual({}); + + const response6 = await request(app) + .post('/http/testD') + .timeout(1000) + .send({ test: 'data' }); + expect(response6.status).toBe(403); + expect(response6.body).toEqual({}); + + expect(eventBroker.published.length).toEqual(2); + expect(eventBroker.published[0].topic).toEqual('testA'); + expect(eventBroker.published[0].eventPayload).toEqual({ test: 'data' }); + expect(eventBroker.published[1].topic).toEqual('testB'); + expect(eventBroker.published[1].eventPayload).toEqual({ test: 'data' }); + expect(eventBroker.published[1].metadata).toEqual( + expect.objectContaining({ + 'x-test-signature': 'testB-signature', + }), + ); + }); + + it('without configuration', async () => { + const config = new ConfigReader({}); + + expect(() => + HttpPostIngressEventPublisher.fromConfig({ + config, + logger, + }), + ).not.toThrow(); + }); +}); diff --git a/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts new file mode 100644 index 0000000000..b5a6ccbca1 --- /dev/null +++ b/plugins/events-backend/src/service/http/HttpPostIngressEventPublisher.ts @@ -0,0 +1,124 @@ +/* + * 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 { errorHandler } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { + EventBroker, + EventPublisher, + HttpPostIngressOptions, + RequestValidator, +} from '@backstage/plugin-events-node'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { RequestValidationContextImpl } from './validation'; + +/** + * Publishes events received from their origin (e.g., webhook events from an SCM system) + * via HTTP POST endpoint and passes the request body as event payload to the registered subscribers. + * + * @public + */ +// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.) +export class HttpPostIngressEventPublisher implements EventPublisher { + private eventBroker?: EventBroker; + + static fromConfig(env: { + config: Config; + ingresses?: { [topic: string]: Omit }; + logger: Logger; + }): HttpPostIngressEventPublisher { + const topics = + env.config.getOptionalStringArray('events.http.topics') ?? []; + + const ingresses = env.ingresses ?? {}; + topics.forEach(topic => { + // don't overwrite topic settings + // (e.g., added at the config as well as argument) + if (!ingresses[topic]) { + ingresses[topic] = {}; + } + }); + + return new HttpPostIngressEventPublisher(env.logger, ingresses); + } + + private constructor( + private readonly logger: Logger, + private readonly ingresses: { + [topic: string]: Omit; + }, + ) {} + + bind(router: express.Router): void { + router.use('/http', this.createRouter(this.ingresses)); + } + + async setEventBroker(eventBroker: EventBroker): Promise { + this.eventBroker = eventBroker; + } + + private createRouter(ingresses: { + [topic: string]: Omit; + }): express.Router { + const router = Router(); + router.use(express.json()); + + Object.keys(ingresses).forEach(topic => + this.addRouteForTopic(router, topic, ingresses[topic].validator), + ); + + router.use(errorHandler()); + return router; + } + + private addRouteForTopic( + router: express.Router, + topic: string, + validator?: RequestValidator, + ): void { + const path = `/${topic}`; + + router.post(path, async (request, response) => { + const requestDetails = { + body: request.body, + headers: request.headers, + }; + const context = new RequestValidationContextImpl(); + await validator?.(requestDetails, context); + if (context.wasRejected()) { + response + .status(context.rejectionDetails!.status) + .json(context.rejectionDetails!.payload); + return; + } + + const eventPayload = request.body; + await this.eventBroker!.publish({ + topic, + eventPayload, + metadata: request.headers, + }); + + response.status(202).json({ status: 'accepted' }); + }); + + // TODO(pjungermann): We don't really know the externally defined path prefix here, + // however it is more useful for users to have it. Is there a better way? + this.logger.info(`Registered /api/events/http${path} to receive events`); + } +} diff --git a/plugins/events-backend/src/service/http/index.ts b/plugins/events-backend/src/service/http/index.ts new file mode 100644 index 0000000000..fe71e49dfa --- /dev/null +++ b/plugins/events-backend/src/service/http/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { HttpPostIngressEventPublisher } from './HttpPostIngressEventPublisher'; +export * from './validation'; diff --git a/plugins/events-backend/src/service/http/validation/RequestValidationContextImpl.test.ts b/plugins/events-backend/src/service/http/validation/RequestValidationContextImpl.test.ts new file mode 100644 index 0000000000..b2f1485f65 --- /dev/null +++ b/plugins/events-backend/src/service/http/validation/RequestValidationContextImpl.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { RequestValidationContextImpl } from './RequestValidationContextImpl'; + +describe('RequestValidationContextImpl', () => { + it('not rejected', () => { + const context = new RequestValidationContextImpl(); + + expect(context.wasRejected()).toBe(false); + expect(context.rejectionDetails).toBeUndefined(); + }); + + it('reject without details', () => { + const context = new RequestValidationContextImpl(); + + context.reject(); + + expect(context.wasRejected()).toBe(true); + expect(context.rejectionDetails).not.toBeUndefined(); + expect(context.rejectionDetails!.status).toBe(403); + expect(context.rejectionDetails!.payload).toEqual({}); + }); + + it('reject with partial details', () => { + const context = new RequestValidationContextImpl(); + + context.reject({ status: 404 }); + + expect(context.wasRejected()).toBe(true); + expect(context.rejectionDetails).not.toBeUndefined(); + expect(context.rejectionDetails!.status).toBe(404); + expect(context.rejectionDetails!.payload).toEqual({}); + }); + + it('reject with details', () => { + const context = new RequestValidationContextImpl(); + + context.reject({ + status: 403, + payload: { message: 'invalid signature' }, + }); + + expect(context.wasRejected()).toBe(true); + expect(context.rejectionDetails).not.toBeUndefined(); + expect(context.rejectionDetails!.status).toBe(403); + expect(context.rejectionDetails!.payload).toEqual({ + message: 'invalid signature', + }); + }); +}); diff --git a/plugins/events-backend/src/service/http/validation/RequestValidationContextImpl.ts b/plugins/events-backend/src/service/http/validation/RequestValidationContextImpl.ts new file mode 100644 index 0000000000..3dc607a683 --- /dev/null +++ b/plugins/events-backend/src/service/http/validation/RequestValidationContextImpl.ts @@ -0,0 +1,39 @@ +/* + * 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 { + RequestRejectionDetails, + RequestValidationContext, +} from '@backstage/plugin-events-node'; + +export class RequestValidationContextImpl implements RequestValidationContext { + #rejectionDetails: RequestRejectionDetails | undefined; + + reject(details?: Partial): void { + this.#rejectionDetails = { + status: details?.status ?? 403, + payload: details?.payload ?? {}, + }; + } + + wasRejected(): boolean { + return this.#rejectionDetails !== undefined; + } + + get rejectionDetails() { + return this.#rejectionDetails; + } +} diff --git a/plugins/events-backend/src/service/http/validation/index.ts b/plugins/events-backend/src/service/http/validation/index.ts new file mode 100644 index 0000000000..7513014906 --- /dev/null +++ b/plugins/events-backend/src/service/http/validation/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { RequestValidationContextImpl } from './RequestValidationContextImpl'; diff --git a/plugins/events-backend/src/setupTests.ts b/plugins/events-backend/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/events-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/events-node/.eslintrc.js b/plugins/events-node/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/events-node/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md new file mode 100644 index 0000000000..15f411de9b --- /dev/null +++ b/plugins/events-node/CHANGELOG.md @@ -0,0 +1,53 @@ +# @backstage/plugin-events-node + +## 0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.1 + +## 0.2.0-next.0 + +### Minor Changes + +- cf41eedf43: Introduce a new interface `RequestDetails` to abstract `Request` + providing access to request body and headers. + + **BREAKING:** Replace `request: Request` with `request: RequestDetails` at `RequestValidator`. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.5-next.0 + +## 0.1.0 + +### Minor Changes + +- dc9da28abd: Support events received via HTTP endpoints at plugin-events-backend. + + The plugin provides an event publisher `HttpPostIngressEventPublisher` + which will allow you to receive events via + HTTP endpoints `POST /api/events/http/{topic}` + and will publish these to the used event broker. + + Using a provided custom validator, you can participate in the decision + which events are accepted, e.g. by verifying the source of the request. + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md. + +- 7bbd2403a1: Adds a new backend plugin plugin-events-backend for managing events. + + plugin-events-node exposes interfaces which can be used by modules. + + plugin-events-backend-test-utils provides utilities which can be used while writing tests e.g. for modules. + + Please find more information at + https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.1.4 diff --git a/plugins/events-node/README.md b/plugins/events-node/README.md new file mode 100644 index 0000000000..44738222bc --- /dev/null +++ b/plugins/events-node/README.md @@ -0,0 +1,3 @@ +# plugin-events-node + +Houses types and utilities for building events-related modules. diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md new file mode 100644 index 0000000000..51136fd301 --- /dev/null +++ b/plugins/events-node/api-report.md @@ -0,0 +1,111 @@ +## API Report File for "@backstage/plugin-events-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ExtensionPoint } from '@backstage/backend-plugin-api'; + +// @public +export interface EventBroker { + publish(params: EventParams): Promise; + subscribe( + ...subscribers: Array> + ): void; +} + +// @public (undocumented) +export interface EventParams { + eventPayload: unknown; + metadata?: Record; + topic: string; +} + +// @public +export interface EventPublisher { + // (undocumented) + setEventBroker(eventBroker: EventBroker): Promise; +} + +// @public +export abstract class EventRouter implements EventPublisher, EventSubscriber { + // (undocumented) + protected abstract determineDestinationTopic( + params: EventParams, + ): string | undefined; + // (undocumented) + onEvent(params: EventParams): Promise; + // (undocumented) + setEventBroker(eventBroker: EventBroker): Promise; + // (undocumented) + abstract supportsEventTopics(): string[]; +} + +// @alpha (undocumented) +export interface EventsExtensionPoint { + // (undocumented) + addHttpPostIngress(options: HttpPostIngressOptions): void; + // (undocumented) + addPublishers( + ...publishers: Array> + ): void; + // (undocumented) + addSubscribers( + ...subscribers: Array> + ): void; + // (undocumented) + setEventBroker(eventBroker: EventBroker): void; +} + +// @alpha (undocumented) +export const eventsExtensionPoint: ExtensionPoint; + +// @public +export interface EventSubscriber { + onEvent(params: EventParams): Promise; + supportsEventTopics(): string[]; +} + +// @public (undocumented) +export interface HttpPostIngressOptions { + // (undocumented) + topic: string; + // (undocumented) + validator?: RequestValidator; +} + +// @public (undocumented) +export interface RequestDetails { + body: unknown; + headers: Record; +} + +// @public +export interface RequestRejectionDetails { + // (undocumented) + payload: unknown; + // (undocumented) + status: number; +} + +// @public +export interface RequestValidationContext { + reject(details?: Partial): void; +} + +// @public +export type RequestValidator = ( + request: RequestDetails, + context: RequestValidationContext, +) => Promise; + +// @public +export abstract class SubTopicEventRouter extends EventRouter { + protected constructor(topic: string); + // (undocumented) + protected determineDestinationTopic(params: EventParams): string | undefined; + // (undocumented) + protected abstract determineSubTopic(params: EventParams): string | undefined; + // (undocumented) + supportsEventTopics(): string[]; +} +``` diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json new file mode 100644 index 0000000000..490304769c --- /dev/null +++ b/plugins/events-node/package.json @@ -0,0 +1,36 @@ +{ + "name": "@backstage/plugin-events-node", + "description": "The plugin-events-node module for @backstage/plugin-events-backend", + "version": "0.2.0-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "alphaTypes": "dist/index.alpha.d.ts", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build --experimental-type-build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "alpha", + "dist" + ] +} diff --git a/plugins/events-node/src/api/EventBroker.ts b/plugins/events-node/src/api/EventBroker.ts new file mode 100644 index 0000000000..736c2a2bf0 --- /dev/null +++ b/plugins/events-node/src/api/EventBroker.ts @@ -0,0 +1,43 @@ +/* + * 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 { EventParams } from './EventParams'; +import { EventSubscriber } from './EventSubscriber'; + +/** + * Allows a decoupled and asynchronous communication between components. + * Components can publish events for a given topic and + * others can subscribe for future events for topics they are interested in. + * + * @public + */ +export interface EventBroker { + /** + * Publishes an event for the topic. + * + * @param params - parameters for the to be published event. + */ + publish(params: EventParams): Promise; + + /** + * Adds new subscribers for {@link EventSubscriber#supportsEventTopics | interested topics}. + * + * @param subscribers - interested in events of specified topics. + */ + subscribe( + ...subscribers: Array> + ): void; +} diff --git a/plugins/events-node/src/api/EventParams.ts b/plugins/events-node/src/api/EventParams.ts new file mode 100644 index 0000000000..6912827281 --- /dev/null +++ b/plugins/events-node/src/api/EventParams.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @public + */ +export interface EventParams { + /** + * Topic for which this event should be published. + */ + topic: string; + /** + * Event payload. + */ + eventPayload: unknown; + /** + * Metadata (e.g., HTTP headers and similar for events received from external). + */ + metadata?: Record; +} diff --git a/plugins/events-node/src/api/EventPublisher.ts b/plugins/events-node/src/api/EventPublisher.ts new file mode 100644 index 0000000000..285f427804 --- /dev/null +++ b/plugins/events-node/src/api/EventPublisher.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventBroker } from './EventBroker'; + +/** + * Publishes events to be consumed by subscribers for their topic. + * The events can come from different (external) sources + * like emitted themselves, received via HTTP endpoint (i.e. webhook) + * or from event brokers, queues, etc. + * + * @public + */ +export interface EventPublisher { + setEventBroker(eventBroker: EventBroker): Promise; +} diff --git a/plugins/events-node/src/api/EventRouter.test.ts b/plugins/events-node/src/api/EventRouter.test.ts new file mode 100644 index 0000000000..551c5ea67d --- /dev/null +++ b/plugins/events-node/src/api/EventRouter.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { EventBroker } from './EventBroker'; +import { EventParams } from './EventParams'; +import { EventRouter } from './EventRouter'; + +class TestEventRouter extends EventRouter { + protected determineDestinationTopic(params: EventParams): string | undefined { + const payload = params.eventPayload as { value?: number }; + if (payload.value === undefined) { + return undefined; + } + + return payload.value % 2 === 0 ? 'even' : 'odd'; + } + + supportsEventTopics(): string[] { + return ['my-topic']; + } +} + +describe('EventRouter', () => { + const eventRouter = new TestEventRouter(); + const topic = 'my-topic'; + const metadata = { random: 'metadata' }; + + it('no destination topic', async () => { + const published: EventParams[] = []; + const eventBroker = { + publish: (params: EventParams) => { + published.push(params); + }, + } as EventBroker; + await eventRouter.setEventBroker(eventBroker); + + await eventRouter.onEvent({ + topic, + eventPayload: { discarded: 'event' }, + metadata, + }); + + expect(published).toEqual([]); + }); + + it('with destination topic', async () => { + const published: EventParams[] = []; + const eventBroker = { + publish: (params: EventParams) => { + published.push(params); + }, + } as EventBroker; + await eventRouter.setEventBroker(eventBroker); + + const payloadEven = { value: 2 }; + const payloadOdd = { value: 3 }; + await eventRouter.onEvent({ topic, eventPayload: payloadEven, metadata }); + await eventRouter.onEvent({ topic, eventPayload: payloadOdd, metadata }); + + expect(published.length).toBe(2); + expect(published[0].topic).toEqual('even'); + expect(published[0].eventPayload).toEqual(payloadEven); + expect(published[0].metadata).toEqual(metadata); + expect(published[1].topic).toEqual('odd'); + expect(published[1].eventPayload).toEqual(payloadOdd); + expect(published[1].metadata).toEqual(metadata); + }); +}); diff --git a/plugins/events-node/src/api/EventRouter.ts b/plugins/events-node/src/api/EventRouter.ts new file mode 100644 index 0000000000..b435ef15f4 --- /dev/null +++ b/plugins/events-node/src/api/EventRouter.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventBroker } from './EventBroker'; +import { EventParams } from './EventParams'; +import { EventPublisher } from './EventPublisher'; +import { EventSubscriber } from './EventSubscriber'; + +/** + * Subscribes to a topic and - depending on a set of conditions - + * republishes the event to another topic. + * + * @see {@link https://www.enterpriseintegrationpatterns.com/MessageRouter.html | Message Router pattern}. + * @public + */ +export abstract class EventRouter implements EventPublisher, EventSubscriber { + private eventBroker?: EventBroker; + + protected abstract determineDestinationTopic( + params: EventParams, + ): string | undefined; + + async onEvent(params: EventParams): Promise { + const topic = this.determineDestinationTopic(params); + + if (!topic) { + return; + } + + // republish to different topic + this.eventBroker?.publish({ + ...params, + topic, + }); + } + + async setEventBroker(eventBroker: EventBroker): Promise { + this.eventBroker = eventBroker; + } + + abstract supportsEventTopics(): string[]; +} diff --git a/plugins/events-node/src/api/EventSubscriber.ts b/plugins/events-node/src/api/EventSubscriber.ts new file mode 100644 index 0000000000..439f49b890 --- /dev/null +++ b/plugins/events-node/src/api/EventSubscriber.ts @@ -0,0 +1,38 @@ +/* + * 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 { EventParams } from './EventParams'; + +/** + * Handles received events. + * This may include triggering refreshes of catalog entities + * or other actions to react on events. + * + * @public + */ +export interface EventSubscriber { + /** + * Supported event topics like "github", "bitbucketCloud", etc. + */ + supportsEventTopics(): string[]; + + /** + * React on a received event. + * + * @param params - parameters for the to be received event. + */ + onEvent(params: EventParams): Promise; +} diff --git a/plugins/events-node/src/api/SubTopicEventRouter.test.ts b/plugins/events-node/src/api/SubTopicEventRouter.test.ts new file mode 100644 index 0000000000..d5c79895a5 --- /dev/null +++ b/plugins/events-node/src/api/SubTopicEventRouter.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventBroker } from './EventBroker'; +import { EventParams } from './EventParams'; +import { SubTopicEventRouter } from './SubTopicEventRouter'; + +class TestSubTopicEventRouter extends SubTopicEventRouter { + constructor() { + super('my-topic'); + } + + protected determineSubTopic(params: EventParams): string | undefined { + return params.metadata?.['x-my-event'] as string | undefined; + } +} + +describe('SubTopicEventRouter', () => { + const eventRouter = new TestSubTopicEventRouter(); + const topic = 'my-topic'; + const eventPayload = { test: 'payload' }; + const metadata = { 'x-my-event': 'test.type' }; + + it('no x-my-event', async () => { + const published: EventParams[] = []; + const eventBroker = { + publish: (params: EventParams) => { + published.push(params); + }, + } as EventBroker; + await eventRouter.setEventBroker(eventBroker); + + await eventRouter.onEvent({ topic, eventPayload }); + + expect(published).toEqual([]); + }); + + it('with x-my-event', async () => { + const published: EventParams[] = []; + const eventBroker = { + publish: (params: EventParams) => { + published.push(params); + }, + } as EventBroker; + await eventRouter.setEventBroker(eventBroker); + + await eventRouter.onEvent({ topic, eventPayload, metadata }); + + expect(published.length).toBe(1); + expect(published[0].topic).toEqual('my-topic.test.type'); + expect(published[0].eventPayload).toEqual(eventPayload); + expect(published[0].metadata).toEqual(metadata); + }); +}); diff --git a/plugins/events-node/src/api/SubTopicEventRouter.ts b/plugins/events-node/src/api/SubTopicEventRouter.ts new file mode 100644 index 0000000000..04abe14009 --- /dev/null +++ b/plugins/events-node/src/api/SubTopicEventRouter.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventParams } from './EventParams'; +import { EventRouter } from './EventRouter'; + +/** + * Subscribes to the provided (generic) topic + * and publishes the events under the more concrete sub-topic + * depending on the implemented logic for determining it. + * Implementing classes might use information from `metadata` + * and/or properties within the payload. + * + * @public + */ +export abstract class SubTopicEventRouter extends EventRouter { + protected constructor(private readonly topic: string) { + super(); + } + + protected abstract determineSubTopic(params: EventParams): string | undefined; + + protected determineDestinationTopic(params: EventParams): string | undefined { + const subTopic = this.determineSubTopic(params); + return subTopic ? `${params.topic}.${subTopic}` : undefined; + } + + supportsEventTopics(): string[] { + return [this.topic]; + } +} diff --git a/plugins/events-node/src/api/http/HttpPostIngressOptions.ts b/plugins/events-node/src/api/http/HttpPostIngressOptions.ts new file mode 100644 index 0000000000..d9f005d84d --- /dev/null +++ b/plugins/events-node/src/api/http/HttpPostIngressOptions.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RequestValidator } from './validation'; + +/** + * @public + */ +export interface HttpPostIngressOptions { + topic: string; + validator?: RequestValidator; +} diff --git a/plugins/events-node/src/api/http/index.ts b/plugins/events-node/src/api/http/index.ts new file mode 100644 index 0000000000..9206819a4a --- /dev/null +++ b/plugins/events-node/src/api/http/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { HttpPostIngressOptions } from './HttpPostIngressOptions'; +export * from './validation'; diff --git a/plugins/events-node/src/api/http/validation/RequestDetails.ts b/plugins/events-node/src/api/http/validation/RequestDetails.ts new file mode 100644 index 0000000000..83c2669503 --- /dev/null +++ b/plugins/events-node/src/api/http/validation/RequestDetails.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @public + */ +export interface RequestDetails { + /** + * Request body. JSON payloads have been parsed already. + */ + body: unknown; + /** + * Key-value pairs of header names and values. Header names are lower-cased. + */ + headers: Record; +} diff --git a/plugins/events-node/src/api/http/validation/RequestRejectionDetails.ts b/plugins/events-node/src/api/http/validation/RequestRejectionDetails.ts new file mode 100644 index 0000000000..1eca094367 --- /dev/null +++ b/plugins/events-node/src/api/http/validation/RequestRejectionDetails.ts @@ -0,0 +1,26 @@ +/* + * 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. + */ + +/** + * Details for how to respond to the rejection + * of the received HTTP request transmitting an event payload. + * + * @public + */ +export interface RequestRejectionDetails { + status: number; + payload: unknown; +} diff --git a/plugins/events-node/src/api/http/validation/RequestValidationContext.ts b/plugins/events-node/src/api/http/validation/RequestValidationContext.ts new file mode 100644 index 0000000000..eedb6f1f96 --- /dev/null +++ b/plugins/events-node/src/api/http/validation/RequestValidationContext.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RequestRejectionDetails } from './RequestRejectionDetails'; + +/** + * Passed context for the validation + * at which rejections can be expressed. + * + * @public + */ +export interface RequestValidationContext { + /** + * Rejects the validated request + * + * @param details - Optional details about the rejection which will be provided to the sender. + */ + reject(details?: Partial): void; +} diff --git a/plugins/events-node/src/api/http/validation/RequestValidator.ts b/plugins/events-node/src/api/http/validation/RequestValidator.ts new file mode 100644 index 0000000000..3008db03ae --- /dev/null +++ b/plugins/events-node/src/api/http/validation/RequestValidator.ts @@ -0,0 +1,34 @@ +/* + * 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 { RequestDetails } from './RequestDetails'; +import { RequestValidationContext } from './RequestValidationContext'; + +/** + * Validator used to check the received HTTP request + * transmitting an event payload. + * + * E.g., it can be used for signature verification like + * for GitHub webhook events + * (https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks#secret) + * or other kinds of checks. + * + * @public + */ +export type RequestValidator = ( + request: RequestDetails, + context: RequestValidationContext, +) => Promise; diff --git a/plugins/events-node/src/api/http/validation/index.ts b/plugins/events-node/src/api/http/validation/index.ts new file mode 100644 index 0000000000..11f2e9f1f6 --- /dev/null +++ b/plugins/events-node/src/api/http/validation/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { RequestDetails } from './RequestDetails'; +export type { RequestRejectionDetails } from './RequestRejectionDetails'; +export type { RequestValidationContext } from './RequestValidationContext'; +export type { RequestValidator } from './RequestValidator'; diff --git a/plugins/events-node/src/api/index.ts b/plugins/events-node/src/api/index.ts new file mode 100644 index 0000000000..91711c0e38 --- /dev/null +++ b/plugins/events-node/src/api/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { EventBroker } from './EventBroker'; +export type { EventParams } from './EventParams'; +export type { EventPublisher } from './EventPublisher'; +export { EventRouter } from './EventRouter'; +export type { EventSubscriber } from './EventSubscriber'; +export * from './http'; +export { SubTopicEventRouter } from './SubTopicEventRouter'; diff --git a/plugins/events-node/src/extensions.ts b/plugins/events-node/src/extensions.ts new file mode 100644 index 0000000000..945d86b49c --- /dev/null +++ b/plugins/events-node/src/extensions.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createExtensionPoint } from '@backstage/backend-plugin-api'; +import { + EventBroker, + EventPublisher, + EventSubscriber, + HttpPostIngressOptions, +} from './api'; + +/** + * @alpha + */ +export interface EventsExtensionPoint { + setEventBroker(eventBroker: EventBroker): void; + + addPublishers( + ...publishers: Array> + ): void; + + addSubscribers( + ...subscribers: Array> + ): void; + + addHttpPostIngress(options: HttpPostIngressOptions): void; +} + +/** + * @alpha + */ +export const eventsExtensionPoint = createExtensionPoint({ + id: 'events', +}); diff --git a/plugins/events-node/src/index.ts b/plugins/events-node/src/index.ts new file mode 100644 index 0000000000..422eeb169e --- /dev/null +++ b/plugins/events-node/src/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The events-node module for `@backstage/plugin-events-backend`. + * + * @packageDocumentation + */ + +export * from './api'; +export type { EventsExtensionPoint } from './extensions'; +export { eventsExtensionPoint } from './extensions'; diff --git a/plugins/events-node/src/setupTests.ts b/plugins/events-node/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/events-node/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index df0c013892..736398a303 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,45 @@ # @internal/plugin-todo-list-backend +## 1.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## 1.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 1.0.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + ## 1.0.7-next.0 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index 9d6419a76d..698f18a56b 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.7-next.0", + "version": "1.0.8-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -38,7 +38,7 @@ "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/example-todo-list-common/CHANGELOG.md b/plugins/example-todo-list-common/CHANGELOG.md index 747dd912c0..16a43bbec9 100644 --- a/plugins/example-todo-list-common/CHANGELOG.md +++ b/plugins/example-todo-list-common/CHANGELOG.md @@ -1,5 +1,26 @@ # @internal/plugin-todo-list-common +## 1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.1 + +## 1.0.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.0 + +## 1.0.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.1 + ## 1.0.6-next.0 ### Patch Changes diff --git a/plugins/example-todo-list-common/package.json b/plugins/example-todo-list-common/package.json index 23a16e10c7..5633c65bc1 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.6-next.0", + "version": "1.0.7-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "@backstage/test-utils": "workspace:^", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index f08094181d..081451c85b 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,41 @@ # @internal/plugin-todo-list +## 1.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## 1.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## 1.0.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## 1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + ## 1.0.7-next.0 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index 0b12ae6208..a27df640d2 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.7-next.0", + "version": "1.0.8-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,7 +43,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/example-todo-list/src/components/TodoListPage/TodoListPage.tsx b/plugins/example-todo-list/src/components/TodoListPage/TodoListPage.tsx index 6071502fdb..9185c37419 100644 --- a/plugins/example-todo-list/src/components/TodoListPage/TodoListPage.tsx +++ b/plugins/example-todo-list/src/components/TodoListPage/TodoListPage.tsx @@ -164,7 +164,7 @@ function EditModal({ onSubmit, }: { todo?: Todo; - onSubmit(todo: Todo): any; + onSubmit(t: Todo): any; onCancel(): any; }) { const title = useRef(''); diff --git a/plugins/explore-backend/.eslintrc.js b/plugins/explore-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/explore-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/explore-backend/CHANGELOG.md b/plugins/explore-backend/CHANGELOG.md new file mode 100644 index 0000000000..90fbc1a75c --- /dev/null +++ b/plugins/explore-backend/CHANGELOG.md @@ -0,0 +1,19 @@ +# @backstage/plugin-explore-backend + +## 0.0.1-next.0 + +### Patch Changes + +- 4dec6f16be: Added new `@backstage/plugin-explore-backend` & `@backstage/plugin-explore-common` packages. + + This deprecates the `ExploreToolsConfig` API (in `@backstage/plugin-explore-react`) which is replaced by the `ExploreApi` & `ExploreClient`. The list of `ExploreTool` data can now be provided on the backend by either using the supplied `StaticExploreToolProvider` or by implementing a custom `ExploreToolProvider`. See the [explore-backend README](https://github.com/backstage/backstage/blob/master/plugins/explore-backend/README.md) for full details. + + NOTE: Existing installations that have customized the `ExploreToolConfig` will continue to work through the new `ExploreClient`. However, existing data should be migrated over to new `explore-backend` plugin as `ExploreToolConfig` will be removed in the future. + + BREAKING CHANGE: If you have previously installed the `explore` plugin, but not yet customized the `ExploreToolConfig` API in your `packages/app`, this will result in an empty list of tools. + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-explore-common@0.0.1-next.0 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 diff --git a/plugins/explore-backend/README.md b/plugins/explore-backend/README.md new file mode 100644 index 0000000000..d1714806f4 --- /dev/null +++ b/plugins/explore-backend/README.md @@ -0,0 +1,144 @@ +# explore-backend + +The `explore-backend` plugin provides a backend service for the Explore plugin. +This allows your organizations tools to be surfaced in the Explore plugin +through an API. It also provides a search collator to make it possible to search +for these tools. + +## Getting started + +### Install the Package + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-explore-backend +``` + +### Adding the plugin to your `packages/backend` + +You'll need to add the plugin to the router in your `backend` package. You can +do this by creating a file called `packages/backend/src/plugins/explore.ts` + +```ts +import { + createRouter, + StaticExploreToolProvider, +} from '@backstage/plugin-explore-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +// List of tools you want to surface in the Explore plugin "Tools" page. +const tools: ExploreTool[] = [ + { + title: 'New Relic', + description:'new relic plugin', + url: '/newrelic', + image: 'https://i.imgur.com/L37ikrX.jpg', + tags: ['newrelic', 'proxy', 'nerdGraph'], + }, + ... +]; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + toolProvider: StaticExploreToolProvider.fromData(exploreTools), + }); +} +``` + +With the `explore.ts` router setup in place, add the router to +`packages/backend/src/index.ts`: + +```diff ++import explore from './plugins/explore'; + +async function main() { + ... + const createEnv = makeCreateEnv(config); + + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); ++ const exploreEnv = useHotMemoize(module, () => createEnv('explore')); + + const apiRouter = Router(); ++ apiRouter.use('/explore', await explore(exploreEnv)); + ... + apiRouter.use(notFoundHandler()); +``` + +### Wire up Search Indexing + +To index explore tools you will need to register the search collator in the +`packages/backend/src/plugins/search.ts` file. + +```diff ++import { ToolDocumentCollatorFactory } from '@backstage/plugin-explore-backend'; + +async function createSearchEngine( + env: PluginEnvironment, +): Promise { + ... ++ indexBuilder.addCollator({ ++ schedule, ++ factory: ToolDocumentCollatorFactory.fromConfig(env.config, { ++ discovery: env.discovery, ++ logger: env.logger, ++ }), ++ }); + ... +} +``` + +### Wire up the Frontend + +See [the explore plugin README](../explore/README.md) for more information. + +## Explore Tool Customization + +The `explore-backend` uses the `ExploreToolProvider` interface to provide a list +of tools used in your organization and/or within your Backstage instance. This +can be customized to provide tools from any source. For example you could create +a `CustomExploreToolProvider` that queries an internal for tools in your +`packages/backend/src/plugins/explore.ts` file. + +```ts +import { + createRouter, + StaticExploreToolProvider, +} from '@backstage/plugin-explore-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +class CustomExploreToolProvider implements ExploreToolProvider { + async getTools( + request: GetExploreToolsRequest, + ): Promise { + const externalTools = await queryExternalTools(request); + + const tools: ExploreTool[] = [ + ...externalTools, + // Backstage Tools + { + title: 'New Relic', + description: 'new relic plugin', + url: '/newrelic', + image: 'https://i.imgur.com/L37ikrX.jpg', + tags: ['newrelic', 'proxy', 'nerdGraph'], + }, + ]; + + return { tools }; + } +} + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + return await createRouter({ + logger: env.logger, + toolProvider: new CustomExploreToolProvider(), + }); +} +``` diff --git a/plugins/explore-backend/api-report.md b/plugins/explore-backend/api-report.md new file mode 100644 index 0000000000..cc112ed3cc --- /dev/null +++ b/plugins/explore-backend/api-report.md @@ -0,0 +1,66 @@ +## API Report File for "@backstage/plugin-explore-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { Config } from '@backstage/config'; +import { DocumentCollatorFactory } from '@backstage/plugin-search-common'; +import { ExploreTool } from '@backstage/plugin-explore-common'; +import express from 'express'; +import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; +import { GetExploreToolsResponse } from '@backstage/plugin-explore-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { Logger } from 'winston'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Readable } from 'stream'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export interface ExploreToolProvider { + getTools(request: GetExploreToolsRequest): Promise; +} + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + logger: Logger; + // (undocumented) + toolProvider: ExploreToolProvider; +} + +// @public +export class StaticExploreToolProvider implements ExploreToolProvider { + // (undocumented) + static fromData(tools: ExploreTool[]): StaticExploreToolProvider; + // (undocumented) + getTools(request: GetExploreToolsRequest): Promise; +} + +// @public +export interface ToolDocument extends IndexableDocument, ExploreTool {} + +// @public +export class ToolDocumentCollatorFactory implements DocumentCollatorFactory { + // (undocumented) + execute(): AsyncGenerator; + // (undocumented) + static fromConfig( + _config: Config, + options: ToolDocumentCollatorFactoryOptions, + ): ToolDocumentCollatorFactory; + // (undocumented) + getCollator(): Promise; + // (undocumented) + readonly type: string; +} + +// @public +export type ToolDocumentCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + logger: Logger; +}; +``` diff --git a/plugins/explore-backend/package.json b/plugins/explore-backend/package.json new file mode 100644 index 0000000000..8a23c22856 --- /dev/null +++ b/plugins/explore-backend/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-explore-backend", + "version": "0.0.1-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-explore-common": "workspace:^", + "@backstage/plugin-search-common": "workspace:^", + "@types/express": "*", + "express": "^4.18.1", + "express-promise-router": "^4.1.0", + "lodash": "^4.17.21", + "node-fetch": "^2.6.7", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/supertest": "^2.0.8", + "msw": "^0.47.0", + "supertest": "^6.2.4" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/explore-backend/src/example/exampleTools.ts b/plugins/explore-backend/src/example/exampleTools.ts new file mode 100644 index 0000000000..3726e4c5dd --- /dev/null +++ b/plugins/explore-backend/src/example/exampleTools.ts @@ -0,0 +1,98 @@ +/* + * 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 { ExploreTool } from '@backstage/plugin-explore-common'; + +/** + * @private Example only - do not use in production + */ +export const exampleTools: ExploreTool[] = [ + { + title: 'New Relic', + description: + 'Observability platform built to help engineers create and monitor their software', + url: '/newrelic', + image: 'https://i.imgur.com/L37ikrX.jpg', + tags: ['newrelic', 'performance', 'monitoring', 'errors', 'alerting'], + }, + { + title: 'CircleCI', + description: + 'Provides builds overview, detailed build info and retriggering functionality for CircleCI.', + url: '/circleci', + image: 'https://miro.medium.com/max/1200/1*hkTBp22vLAqlIHkrkZHPnw.png', + tags: ['circleci', 'ci', 'dev'], + }, + { + title: 'Sentry', + description: + 'Self-hosted and cloud-based error monitoring that helps software teams discover, triage, and prioritize errors in real-time.', + url: '/sentry', + image: 'https://sentry-brand.storage.googleapis.com/sentry-logo-black.png', + tags: ['sentry', 'monitoring', 'errors'], + }, + { + title: 'Lighthouse', + description: + "Google's Lighthouse tool is a great resource for benchmarking and improving the accessibility, performance, SEO, and best practices of your website.", + url: '/lighthouse', + image: + 'https://raw.githubusercontent.com/GoogleChrome/lighthouse/8b3d7f052b2e64dd857e741d7395647f487697e7/assets/lighthouse-logo.png', + tags: ['web', 'seo', 'accessibility', 'performance'], + }, + { + title: 'Tech Radar', + description: + 'Tech Radar is a list of technologies, complemented by an assessment result, called ring assignment.', + url: '/tech-radar', + image: + 'https://storage.googleapis.com/wf-blogs-engineering-media/2018/09/fe13bb32-wf-tech-radar-hero-1024x597.png', + tags: ['standards', 'landscape'], + }, + { + title: 'Cost Insights', + description: 'Insights into cloud costs for your organization.', + url: '/cost-insights', + image: 'https://cloud.google.com/images/press/logo-cloud.png', + tags: ['cloud', 'finops'], + }, + { + title: 'GraphiQL', + description: + 'Integrates GraphiQL as a tool to browse GraphiQL endpoints inside Backstage.', + url: '/graphiql', + image: + 'https://camo.githubusercontent.com/517398c3fbe0687d3d4dcbe05da82970b882e75a/68747470733a2f2f64337676366c703535716a6171632e636c6f756466726f6e742e6e65742f6974656d732f33413061324e314c3346324f304c3377326e316a2f477261706869514c382e706e673f582d436c6f75644170702d56697369746f722d49643d3433363432', + tags: ['graphql', 'dev'], + }, + { + title: 'GitOps Clusters', + description: + 'Create GitOps-managed clusters with Backstage. Currently supports EKS flavors and profiles like Machine Learning Ops (MLOps)', + url: '/gitops-clusters', + image: 'https://miro.medium.com/max/801/1*R28u8gj-hVdDFISoYqPhrQ.png', + tags: ['gitops', 'dev'], + }, + { + title: 'Rollbar', + description: + 'Error monitoring and crash reporting for agile development and continuous delivery', + url: '/rollbar', + image: + 'https://images.ctfassets.net/cj4mgtttlyx7/4DfiWj9CbuHBi10uWK7JHn/5e94a6c5dbd5d50bdcd8d9e78f88689b/rollbar-seo.png', + tags: ['rollbar', 'monitoring', 'errors'], + }, +]; diff --git a/plugins/explore-backend/src/index.ts b/plugins/explore-backend/src/index.ts new file mode 100644 index 0000000000..a0e7216621 --- /dev/null +++ b/plugins/explore-backend/src/index.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +/** + * A Backstage plugin for building an exploration page of your software ecosystem + * + * @packageDocumentation + */ + +export * from './search'; +export * from './service'; +export * from './tools'; + +/** + * @internal Example only - do not use in production + */ +export { exampleTools } from './example/exampleTools'; diff --git a/plugins/explore-backend/src/run.ts b/plugins/explore-backend/src/run.ts new file mode 100644 index 0000000000..d945aa13f0 --- /dev/null +++ b/plugins/explore-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts b/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts new file mode 100644 index 0000000000..dd9872acec --- /dev/null +++ b/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts @@ -0,0 +1,101 @@ +/* + * 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 { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; +import { ExploreTool } from '@backstage/plugin-explore-common'; +import { + DocumentCollatorFactory, + IndexableDocument, +} from '@backstage/plugin-search-common'; +import fetch from 'node-fetch'; +import { Readable } from 'stream'; +import { Logger } from 'winston'; + +/** + * Extended IndexableDocument with explore tool specific properties + * + * @public + */ +export interface ToolDocument extends IndexableDocument, ExploreTool {} + +/** + * The options for the {@link ToolDocumentCollatorFactory}. + * + * @public + */ +export type ToolDocumentCollatorFactoryOptions = { + discovery: PluginEndpointDiscovery; + logger: Logger; +}; + +/** + * Search collator responsible for collecting explore tools to index. + * + * @public + */ +export class ToolDocumentCollatorFactory implements DocumentCollatorFactory { + public readonly type: string = 'tools'; + + private readonly discovery: PluginEndpointDiscovery; + private readonly logger: Logger; + + private constructor(options: ToolDocumentCollatorFactoryOptions) { + this.discovery = options.discovery; + this.logger = options.logger; + } + + static fromConfig( + _config: Config, + options: ToolDocumentCollatorFactoryOptions, + ) { + return new ToolDocumentCollatorFactory(options); + } + + async getCollator() { + return Readable.from(this.execute()); + } + + async *execute(): AsyncGenerator { + this.logger.info('Starting collation of explore tools'); + + const tools = await this.fetchTools(); + + for (const tool of tools) { + yield { + ...tool, + text: tool.description, + location: tool.url, + }; + } + + this.logger.info('Finished collation of explore tools'); + } + + private async fetchTools() { + const baseUrl = await this.discovery.getBaseUrl('explore'); + const response = await fetch(`${baseUrl}/tools`); + + if (!response.ok) { + throw new Error( + `Failed to explore fetch tools, ${response.status}: ${response.statusText}`, + ); + } + + const data = await response.json(); + return data.tools; + } +} diff --git a/plugins/explore-backend/src/search/index.ts b/plugins/explore-backend/src/search/index.ts new file mode 100644 index 0000000000..70e9414652 --- /dev/null +++ b/plugins/explore-backend/src/search/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ToolDocumentCollatorFactory } from './ToolDocumentCollatorFactory'; +export type { + ToolDocument, + ToolDocumentCollatorFactoryOptions, +} from './ToolDocumentCollatorFactory'; diff --git a/plugins/explore-backend/src/service/index.ts b/plugins/explore-backend/src/service/index.ts new file mode 100644 index 0000000000..8727bd620b --- /dev/null +++ b/plugins/explore-backend/src/service/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createRouter } from './router'; +export type { RouterOptions } from './router'; diff --git a/plugins/explore-backend/src/service/router.test.ts b/plugins/explore-backend/src/service/router.test.ts new file mode 100644 index 0000000000..5fe5b6c6a3 --- /dev/null +++ b/plugins/explore-backend/src/service/router.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { + ExploreTool, + GetExploreToolsRequest, +} from '@backstage/plugin-explore-common'; +import express from 'express'; +import request from 'supertest'; +import { ExploreToolProvider } from '../tools'; +import { createRouter } from './router'; + +const mockTools: ExploreTool[] = [ + { title: 'Tool 1', url: 'https://example.com/tool1', image: '' }, + { title: 'Tool 2', url: 'https://example.com/tool2', image: '' }, + { title: 'Tool 3', url: 'https://example.com/tool2', image: '' }, +]; + +describe('createRouter', () => { + let app: express.Express; + const toolProvider: ExploreToolProvider = { + getTools: async ({}: GetExploreToolsRequest) => { + return { + tools: mockTools, + }; + }, + }; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + toolProvider, + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /tools', () => { + it('returns ok', async () => { + const response = await request(app).get('/tools'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual( + expect.objectContaining({ tools: mockTools }), + ); + }); + }); +}); diff --git a/plugins/explore-backend/src/service/router.ts b/plugins/explore-backend/src/service/router.ts new file mode 100644 index 0000000000..393736529a --- /dev/null +++ b/plugins/explore-backend/src/service/router.ts @@ -0,0 +1,65 @@ +/* + * 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 { errorHandler } from '@backstage/backend-common'; +import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { ExploreToolProvider } from '../tools'; + +/** + * @public + */ +export interface RouterOptions { + logger: Logger; + toolProvider: ExploreToolProvider; +} + +/** + * @public + */ +export async function createRouter( + options: RouterOptions, +): Promise { + const { toolProvider } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/tools', async (request, response) => { + const requestQuery = parseExploreToolRequestQuery(request.query); + const result = await toolProvider.getTools(requestQuery); + response.json(result); + }); + + router.use(errorHandler()); + + return router; +} + +function parseExploreToolRequestQuery( + params: Record, +): GetExploreToolsRequest { + // TODO: Implement proper query parsing for the filters + // Also consider using a `filter` query string like catalog client does + return { + filter: { + tags: [...[(params?.tag as any) ?? []]].flat(), + lifecycle: [...[(params?.lifecycle as any) ?? []]].flat(), + }, + }; +} diff --git a/plugins/explore-backend/src/service/standaloneServer.ts b/plugins/explore-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..6cf5852f18 --- /dev/null +++ b/plugins/explore-backend/src/service/standaloneServer.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createServiceBuilder } from '@backstage/backend-common'; +import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ + service: 'explore-backend', + }); + + logger.debug('Starting application server...'); + + const router = await createRouter({ + logger, + toolProvider: { + getTools: async ({}: GetExploreToolsRequest) => { + return { + filter: undefined, + tools: [ + { title: 'Tool 1', url: 'https://example.com/tool1', image: '' }, + { title: 'Tool 2', url: 'https://example.com/tool2', image: '' }, + { title: 'Tool 3', url: 'https://example.com/tool2', image: '' }, + ], + }; + }, + }, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/explore-backend', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/explore-backend/src/setupTests.ts b/plugins/explore-backend/src/setupTests.ts new file mode 100644 index 0000000000..813cdeaae3 --- /dev/null +++ b/plugins/explore-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/explore-backend/src/tools/index.ts b/plugins/explore-backend/src/tools/index.ts new file mode 100644 index 0000000000..5e605e0142 --- /dev/null +++ b/plugins/explore-backend/src/tools/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { StaticExploreToolProvider } from './providers/StaticExploreToolProvider'; +export * from './types'; diff --git a/plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.test.ts b/plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.test.ts new file mode 100644 index 0000000000..626426eed9 --- /dev/null +++ b/plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { ExploreTool } from '@backstage/plugin-explore-common'; +import { StaticExploreToolProvider } from './StaticExploreToolProvider'; + +describe('StaticExploreToolProvider', () => { + const tool1: ExploreTool = { + title: 'Tool 1', + image: 'https://example/image.png', + url: 'https://example.com', + lifecycle: 'production', + tags: ['tag1', 'tag2'], + }; + const tool2: ExploreTool = { + ...tool1, + title: 'Tool 2', + lifecycle: 'production', + tags: ['tag1'], + }; + const tool3: ExploreTool = { + ...tool1, + title: 'Tool 3', + lifecycle: 'experimental', + tags: ['tag2'], + }; + const allTools: ExploreTool[] = [tool1, tool2, tool3]; + + describe('getTools', () => { + it('returns a list of all tools', async () => { + const provider = StaticExploreToolProvider.fromData(allTools); + + await expect(provider.getTools({})).resolves.toEqual({ + tools: allTools, + }); + }); + + it.each([ + [['tag1'], [tool1, tool2]], + [['tag2'], [tool1, tool3]], + [[], allTools], + ])( + 'returns % when filtered by tags %', + async (tagFilter, expectedTools) => { + const provider = StaticExploreToolProvider.fromData(allTools); + + await expect( + provider.getTools({ filter: { tags: tagFilter } }), + ).resolves.toEqual({ + tools: expectedTools, + }); + }, + ); + + it.each([ + [['production'], [tool1, tool2]], + [['experimental'], [tool3]], + [[], allTools], + ])( + 'returns % when filtered by lifecycle %', + async (lifecycleFilter, expectedTools) => { + const provider = StaticExploreToolProvider.fromData(allTools); + + await expect( + provider.getTools({ filter: { lifecycle: lifecycleFilter } }), + ).resolves.toEqual({ + tools: expectedTools, + }); + }, + ); + }); +}); diff --git a/plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.ts b/plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.ts new file mode 100644 index 0000000000..ad8bcafada --- /dev/null +++ b/plugins/explore-backend/src/tools/providers/StaticExploreToolProvider.ts @@ -0,0 +1,59 @@ +/* + * 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 { + ExploreTool, + GetExploreToolsRequest, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; +import { intersection, isEmpty } from 'lodash'; +import { ExploreToolProvider } from '../types'; + +const anyOf = (prop: T | T[], matches: T[]) => + isEmpty(matches) + ? true + : intersection([...[prop]].flat(), matches)?.length > 0; + +/** + * A basic ExploreToolProvider implementation using static data. + * + * @public + */ +export class StaticExploreToolProvider implements ExploreToolProvider { + private readonly tools: ExploreTool[]; + + static fromData(tools: ExploreTool[]) { + return new StaticExploreToolProvider(tools); + } + + private constructor(tools: ExploreTool[]) { + this.tools = tools; + } + + async getTools( + request: GetExploreToolsRequest, + ): Promise { + const { filter } = request ?? {}; + const tags = filter?.tags ?? []; + const lifecycles = filter?.lifecycle ?? []; + + const tools = this.tools.filter( + t => anyOf(t.tags ?? [], tags) && anyOf(t.lifecycle ?? [], lifecycles), + ); + + return { tools }; + } +} diff --git a/plugins/explore-backend/src/tools/types.ts b/plugins/explore-backend/src/tools/types.ts new file mode 100644 index 0000000000..3870924a50 --- /dev/null +++ b/plugins/explore-backend/src/tools/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + GetExploreToolsRequest, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; + +/** + * @public + */ +export interface ExploreToolProvider { + /** + * Gets a list of explore tools filtered by the request options. + * + * @param request - The request options {@link @backstage/plugin-explore-common#GetExploreToolsRequest}. + * @returns The response {@link @backstage/plugin-explore-common#GetExploreToolsResponse}. + */ + getTools(request: GetExploreToolsRequest): Promise; +} diff --git a/plugins/explore-common/.eslintrc.js b/plugins/explore-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/explore-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/explore-common/CHANGELOG.md b/plugins/explore-common/CHANGELOG.md new file mode 100644 index 0000000000..794fe6d995 --- /dev/null +++ b/plugins/explore-common/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-explore-common + +## 0.0.1-next.0 + +### Patch Changes + +- 4dec6f16be: Added new `@backstage/plugin-explore-backend` & `@backstage/plugin-explore-common` packages. + + This deprecates the `ExploreToolsConfig` API (in `@backstage/plugin-explore-react`) which is replaced by the `ExploreApi` & `ExploreClient`. The list of `ExploreTool` data can now be provided on the backend by either using the supplied `StaticExploreToolProvider` or by implementing a custom `ExploreToolProvider`. See the [explore-backend README](https://github.com/backstage/backstage/blob/master/plugins/explore-backend/README.md) for full details. + + NOTE: Existing installations that have customized the `ExploreToolConfig` will continue to work through the new `ExploreClient`. However, existing data should be migrated over to new `explore-backend` plugin as `ExploreToolConfig` will be removed in the future. + + BREAKING CHANGE: If you have previously installed the `explore` plugin, but not yet customized the `ExploreToolConfig` API in your `packages/app`, this will result in an empty list of tools. diff --git a/plugins/explore-common/README.md b/plugins/explore-common/README.md new file mode 100644 index 0000000000..7e8c835660 --- /dev/null +++ b/plugins/explore-common/README.md @@ -0,0 +1,3 @@ +# explore-common + +Shared types for the `explore` plugin. diff --git a/plugins/explore-common/api-report.md b/plugins/explore-common/api-report.md new file mode 100644 index 0000000000..c9a552267c --- /dev/null +++ b/plugins/explore-common/api-report.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/plugin-explore-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public (undocumented) +export type ExploreTool = { + title: string; + description?: string; + url: string; + image: string; + tags?: string[]; + lifecycle?: string; +}; + +// @public (undocumented) +export type ExploreToolFilter = { + tags?: string[]; + lifecycle?: string[]; +}; + +// @public (undocumented) +export type GetExploreToolsRequest = { + filter?: ExploreToolFilter; +}; + +// @public (undocumented) +export type GetExploreToolsResponse = { + tools: ExploreTool[]; +}; +``` diff --git a/plugins/explore-common/package.json b/plugins/explore-common/package.json new file mode 100644 index 0000000000..5887a26d06 --- /dev/null +++ b/plugins/explore-common/package.json @@ -0,0 +1,39 @@ +{ + "name": "@backstage/plugin-explore-common", + "description": "Common functionalities for the explore plugin", + "version": "0.0.1-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/explore-common" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/explore-common/src/index.ts b/plugins/explore-common/src/index.ts new file mode 100644 index 0000000000..d829a62e63 --- /dev/null +++ b/plugins/explore-common/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common functionalities for the explore plugin. + * + * @packageDocumentation + */ + +export * from './tools'; diff --git a/plugins/explore-common/src/tools/api.ts b/plugins/explore-common/src/tools/api.ts new file mode 100644 index 0000000000..256671c1af --- /dev/null +++ b/plugins/explore-common/src/tools/api.ts @@ -0,0 +1,46 @@ +/* + * 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 { ExploreTool } from './types'; + +/** + * @public + */ +export type ExploreToolFilter = { + /** + * Filter by a list of tags + */ + tags?: string[]; + + /** + * Filter by a list of lifecycle strings + */ + lifecycle?: string[]; +}; + +/** + * @public + */ +export type GetExploreToolsRequest = { + filter?: ExploreToolFilter; +}; + +/** + * @public + */ +export type GetExploreToolsResponse = { + tools: ExploreTool[]; +}; diff --git a/plugins/explore-common/src/tools/index.ts b/plugins/explore-common/src/tools/index.ts new file mode 100644 index 0000000000..50a9b9f649 --- /dev/null +++ b/plugins/explore-common/src/tools/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common functionalities for the explore plugin. + * + * @packageDocumentation + */ + +export * from './api'; +export * from './types'; diff --git a/plugins/explore-common/src/tools/types.ts b/plugins/explore-common/src/tools/types.ts new file mode 100644 index 0000000000..25f995f807 --- /dev/null +++ b/plugins/explore-common/src/tools/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @public + */ +export type ExploreTool = { + title: string; + description?: string; + url: string; + image: string; + tags?: string[]; + lifecycle?: string; +}; diff --git a/plugins/explore-react/CHANGELOG.md b/plugins/explore-react/CHANGELOG.md index c139c37348..f4f4b98ebe 100644 --- a/plugins/explore-react/CHANGELOG.md +++ b/plugins/explore-react/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-explore-react +## 0.0.24-next.1 + +### Patch Changes + +- 4dec6f16be: Added new `@backstage/plugin-explore-backend` & `@backstage/plugin-explore-common` packages. + + This deprecates the `ExploreToolsConfig` API (in `@backstage/plugin-explore-react`) which is replaced by the `ExploreApi` & `ExploreClient`. The list of `ExploreTool` data can now be provided on the backend by either using the supplied `StaticExploreToolProvider` or by implementing a custom `ExploreToolProvider`. See the [explore-backend README](https://github.com/backstage/backstage/blob/master/plugins/explore-backend/README.md) for full details. + + NOTE: Existing installations that have customized the `ExploreToolConfig` will continue to work through the new `ExploreClient`. However, existing data should be migrated over to new `explore-backend` plugin as `ExploreToolConfig` will be removed in the future. + + BREAKING CHANGE: If you have previously installed the `explore` plugin, but not yet customized the `ExploreToolConfig` API in your `packages/app`, this will result in an empty list of tools. + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-explore-common@0.0.1-next.0 + +## 0.0.24-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.0 + +## 0.0.23 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.0 + ## 0.0.23-next.0 ### Patch Changes diff --git a/plugins/explore-react/api-report.md b/plugins/explore-react/api-report.md index 8a9d2b8b13..61b3cbee2d 100644 --- a/plugins/explore-react/api-report.md +++ b/plugins/explore-react/api-report.md @@ -4,23 +4,17 @@ ```ts import { ApiRef } from '@backstage/core-plugin-api'; +import * as common from '@backstage/plugin-explore-common'; -// @public (undocumented) -export type ExploreTool = { - title: string; - description?: string; - url: string; - image: string; - tags?: string[]; - lifecycle?: string; -}; +// @public @deprecated (undocumented) +export type ExploreTool = common.ExploreTool; -// @public (undocumented) +// @public @deprecated (undocumented) export interface ExploreToolsConfig { // (undocumented) getTools: () => Promise; } -// @public (undocumented) +// @public @deprecated (undocumented) export const exploreToolsConfigRef: ApiRef; ``` diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 27ee4c23df..38503c7f5f 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore-react", "description": "A frontend library for Backstage plugins that want to interact with the explore plugin", - "version": "0.0.23-next.0", + "version": "0.0.24-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,8 @@ "start": "backstage-cli package start" }, "dependencies": { - "@backstage/core-plugin-api": "workspace:^" + "@backstage/core-plugin-api": "workspace:^", + "@backstage/plugin-explore-common": "workspace:^" }, "devDependencies": { "@backstage/cli": "workspace:^", @@ -43,7 +44,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/explore-react/src/tools/api.ts b/plugins/explore-react/src/tools/api.ts index 0fae2ce0fb..32dccfb5dd 100644 --- a/plugins/explore-react/src/tools/api.ts +++ b/plugins/explore-react/src/tools/api.ts @@ -16,22 +16,29 @@ import { createApiRef } from '@backstage/core-plugin-api'; -/** @public */ +/** + * Deprecated types moved to `@backstage/plugin-explore-common` + */ +import * as common from '@backstage/plugin-explore-common'; + +/** + * @deprecated Use ExploreTool from `@backstage/plugin-explore-common` + * @public + */ +export type ExploreTool = common.ExploreTool; + +/** + * @deprecated Use exploreApiRef from `@backstage/plugin-explore` & the `@backstage/plugin-explore-backend` + * @public + */ export const exploreToolsConfigRef = createApiRef({ id: 'plugin.explore.toolsconfig', }); -/** @public */ -export type ExploreTool = { - title: string; - description?: string; - url: string; - image: string; - tags?: string[]; - lifecycle?: string; -}; - -/** @public */ +/** + * @deprecated Use ExploreApi from `@backstage/plugin-explore` & the `@backstage/plugin-explore-backend` + * @public + */ export interface ExploreToolsConfig { getTools: () => Promise; } diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index bc31cd1ec6..4b4f15a668 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,69 @@ # @backstage/plugin-explore +## 0.3.43-next.1 + +### Patch Changes + +- 4dec6f16be: Added new `@backstage/plugin-explore-backend` & `@backstage/plugin-explore-common` packages. + + This deprecates the `ExploreToolsConfig` API (in `@backstage/plugin-explore-react`) which is replaced by the `ExploreApi` & `ExploreClient`. The list of `ExploreTool` data can now be provided on the backend by either using the supplied `StaticExploreToolProvider` or by implementing a custom `ExploreToolProvider`. See the [explore-backend README](https://github.com/backstage/backstage/blob/master/plugins/explore-backend/README.md) for full details. + + NOTE: Existing installations that have customized the `ExploreToolConfig` will continue to work through the new `ExploreClient`. However, existing data should be migrated over to new `explore-backend` plugin as `ExploreToolConfig` will be removed in the future. + + BREAKING CHANGE: If you have previously installed the `explore` plugin, but not yet customized the `ExploreToolConfig` API in your `packages/app`, this will result in an empty list of tools. + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/plugin-explore-common@0.0.1-next.0 + - @backstage/plugin-explore-react@0.0.24-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 0.3.43-next.0 + +### Patch Changes + +- ea4a5be8f3: Adds styling to graph forcing it to always fill out the available space. +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-explore-react@0.0.24-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.3.42 + +### Patch Changes + +- 5c25ce6d9e: Added a section to explore plugin README that describes the customization of explore tools content. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-explore-react@0.0.23 + +## 0.3.42-next.1 + +### Patch Changes + +- 5c25ce6d9e: Added a section to explore plugin README that describes the customization of explore tools content. +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-explore-react@0.0.23-next.0 + ## 0.3.42-next.0 ### Patch Changes diff --git a/plugins/explore/README.md b/plugins/explore/README.md index 8e1cbaed02..0204f34714 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -1,42 +1,105 @@ # explore Welcome to the explore plugin! + This plugin helps to visualize the domains and tools in your ecosystem. -## Getting started +## Setup -To install the plugin, add and bind the route in `App.tsx`: +The following sections will help you get the Explore plugin setup and running. -```typescript -import { ExplorePage, explorePlugin } from '@backstage/plugin-explore'; +### Backend -... +You need to setup the +[Explore backend plugin](https://github.com/backstage/backstage/tree/master/plugins/explore-backend) +before you move forward with any of these steps if you haven't already. -bindRoutes({ bind }) { - ... - bind(explorePlugin.externalRoutes, { - catalogEntity: catalogPlugin.routes.catalogEntity, - }); -}, +### Installation -... +Install this plugin: -} /> +```bash +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-explore ``` -And add a link to the sidebar in `Root.tsx`: +### Add the plugin to your `packages/app` -```typescript -import LayersIcon from '@material-ui/icons/Layers'; +Add the root page that the playlist plugin provides to your app. You can choose +any path for the route, but we recommend the following: + +```diff +// packages/app/src/App.tsx ++ import { ExplorePage, explorePlugin } from '@backstage/plugin-explore'; ... - + + } /> + }> + {entityPage} + ++ } /> + ... + +``` + +You may also want to add a link to the playlist page to your application +sidebar: + +```diff +// packages/app/src/components/Root/Root.tsx ++import LayersIcon from '@material-ui/icons/Layers'; + +export const Root = ({ children }: PropsWithChildren<{}>) => ( + + ++ + ... + +``` + +### Use search result list item for Explore Tools + +When you have your `packages/app/src/components/search/SearchPage.tsx` file +ready to make modifications, add the following code snippet to add the +`ToolSearchResultListItem` when the type of the search results are +`tool`. + +```diff ++import { ToolSearchResultListItem } from '@backstage/plugin-explore'; + +const SearchPage = () => { + ... + + {({ results }) => ( + + {results.map(({ type, document, highlight, rank }) => { + switch (type) { + ... ++ case 'tools': ++ return ( ++ } ++ key={document.location} ++ result={document} ++ highlight={highlight} ++ rank={rank} ++ /> ++ ); + } + })} + + )} + ... + +... ``` ## Customization -Create a custom explore page in `packages/app/src/components/explore/ExplorePage.tsx`. +Create a custom explore page in +`packages/app/src/components/explore/ExplorePage.tsx`. ```tsx import { @@ -68,7 +131,7 @@ export const explorePage = ; Now register the new explore page in `packages/app/src/App.tsx`. ```diff -+ import { explorePage } from './components/explore/ExplorePage'; ++import { explorePage } from './components/explore/ExplorePage'; const routes = ( diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index 9653c0fb63..d184ce01a4 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -5,10 +5,19 @@ ```ts /// +import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; import { DomainEntity } from '@backstage/catalog-model'; +import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { FetchApi } from '@backstage/core-plugin-api'; +import { GetExploreToolsRequest } from '@backstage/plugin-explore-common'; +import { GetExploreToolsResponse } from '@backstage/plugin-explore-common'; +import { IndexableDocument } from '@backstage/plugin-search-common'; +import { ReactNode } from 'react'; +import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core'; @@ -30,6 +39,29 @@ export const DomainExplorerContent: (props: { title?: string | undefined; }) => JSX.Element; +// @public +export interface ExploreApi { + getTools(request?: GetExploreToolsRequest): Promise; +} + +// @public (undocumented) +export const exploreApiRef: ApiRef; + +// @public +export class ExploreClient implements ExploreApi { + constructor({ + discoveryApi, + fetchApi, + exploreToolsConfig, + }: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + exploreToolsConfig?: ExploreToolsConfig; + }); + // (undocumented) + getTools(request?: GetExploreToolsRequest): Promise; +} + // @public export const ExploreLayout: { (props: ExploreLayoutProps): JSX.Element; @@ -91,4 +123,21 @@ export type SubRoute = { export const ToolExplorerContent: (props: { title?: string | undefined; }) => JSX.Element; + +// @public (undocumented) +export function ToolSearchResultListItem( + props: ToolSearchResultListItemProps, +): JSX.Element; + +// @public +export interface ToolSearchResultListItemProps { + // (undocumented) + highlight?: ResultHighlight; + // (undocumented) + icon?: ReactNode; + // (undocumented) + rank?: number; + // (undocumented) + result: IndexableDocument; +} ``` diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 7ff382b9ce..793628a0bd 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore", "description": "A Backstage plugin for building an exploration page of your software ecosystem", - "version": "0.3.42-next.0", + "version": "0.3.43-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,8 +35,12 @@ "@backstage/catalog-model": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-explore-common": "workspace:^", "@backstage/plugin-explore-react": "workspace:^", + "@backstage/plugin-search-common": "workspace:^", + "@backstage/plugin-search-react": "workspace:^", "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -61,7 +65,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/explore/src/api/ExploreApi.ts b/plugins/explore/src/api/ExploreApi.ts new file mode 100644 index 0000000000..ef9eb646b7 --- /dev/null +++ b/plugins/explore/src/api/ExploreApi.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core-plugin-api'; +import { + GetExploreToolsRequest, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; + +/** + * Interface for the explore plugin. + * + * @public + */ +export interface ExploreApi { + /** + * Returns a list of explore tools. + * + * @param request - The The request query options + */ + getTools(request?: GetExploreToolsRequest): Promise; +} + +/** + * @public + */ +export const exploreApiRef = createApiRef({ + id: 'plugin.explore.service', +}); diff --git a/plugins/explore/src/api/ExploreClient.test.ts b/plugins/explore/src/api/ExploreClient.test.ts new file mode 100644 index 0000000000..bbec568a09 --- /dev/null +++ b/plugins/explore/src/api/ExploreClient.test.ts @@ -0,0 +1,114 @@ +/* + * 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 { + ExploreTool, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; +import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { ExploreClient } from './ExploreClient'; + +const server = setupServer(); + +describe('ExploreClient', () => { + setupRequestMockHandlers(server); + + const mockBaseUrl = 'http://backstage/api/explore'; + const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; + const fetchApi = new MockFetchApi(); + + let client: ExploreClient; + beforeEach(() => { + client = new ExploreClient({ discoveryApi, fetchApi }); + }); + + describe('getTools', () => { + const mockTools: ExploreTool[] = [ + { + title: 'Tool 1', + image: 'https://example.com/image.png', + url: 'https://example.com', + }, + { + title: 'Tool 2', + image: 'https://example.com/image.png', + url: 'https://example.com', + }, + ]; + + it('should fetch data from the explore-backend', async () => { + const expectedResponse: GetExploreToolsResponse = { + tools: mockTools, + }; + + server.use( + rest.get(`${mockBaseUrl}/tools`, (_, res, ctx) => + res(ctx.json(expectedResponse)), + ), + ); + + const response = await client.getTools(); + expect(response).toEqual(expectedResponse); + }); + + it('should request explore tools with specific filters', async () => { + const expectedResponse: GetExploreToolsResponse = { + tools: mockTools, + }; + + server.use( + rest.get(`${mockBaseUrl}/tools`, (req, res, ctx) => { + expect(req.url.search).toBe('?tag=a&tag=b&lifecycle=alpha'); + return res(ctx.json(expectedResponse)); + }), + ); + + const response = await client.getTools({ + filter: { tags: ['a', 'b'], lifecycle: ['alpha'] }, + }); + expect(response).toEqual(expectedResponse); + }); + }); + + describe('when using exploreToolsConfig for backwards compatibility', () => { + const mockExploreToolsConfig = { + getTools: jest.fn(), + }; + + beforeEach(() => { + client = new ExploreClient({ + discoveryApi, + fetchApi, + exploreToolsConfig: mockExploreToolsConfig, + }); + }); + + it('should return data from the deprecated api', async () => { + mockExploreToolsConfig.getTools.mockResolvedValue([ + { + title: 'Some Tool', + image: 'https://example.com/image.png', + url: 'https://example.com', + }, + ]); + + await client.getTools(); + expect(mockExploreToolsConfig.getTools).toHaveBeenCalled(); + }); + }); +}); diff --git a/plugins/explore/src/api/ExploreClient.ts b/plugins/explore/src/api/ExploreClient.ts new file mode 100644 index 0000000000..e913aeb041 --- /dev/null +++ b/plugins/explore/src/api/ExploreClient.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { + GetExploreToolsRequest, + GetExploreToolsResponse, +} from '@backstage/plugin-explore-common'; +import { ExploreToolsConfig } from '@backstage/plugin-explore-react'; +import { ExploreApi } from './ExploreApi'; + +/** + * Default implementation of the ExploreApi. + * + * @public + */ +export class ExploreClient implements ExploreApi { + private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; + // NOTE: This will be removed in the future as it is replaced by the ExploreApi.getTools method + private readonly exploreToolsConfig: ExploreToolsConfig | undefined; + + /** + * @remarks The exploreToolsConfig is for backwards compatibility with the exporeToolsConfigRef + * and will be removed in the future. + */ + constructor({ + discoveryApi, + fetchApi, + exploreToolsConfig = undefined, + }: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + exploreToolsConfig?: ExploreToolsConfig; + }) { + this.discoveryApi = discoveryApi; + this.fetchApi = fetchApi; + this.exploreToolsConfig = exploreToolsConfig; + } + + async getTools( + request: GetExploreToolsRequest = {}, + ): Promise { + // NOTE: This will be removed in the future as it is replaced by the ExploreApi.getTools method + if (this.exploreToolsConfig) { + const tools = await this.exploreToolsConfig.getTools(); + if (tools) { + return { tools }; + } + } + + const { fetch } = this.fetchApi; + + const filter = request.filter ?? {}; + const baseUrl = await this.discoveryApi.getBaseUrl('explore'); + + const tags = filter?.tags?.map(t => `tag=${encodeURIComponent(t)}`) ?? []; + const lifecycles = + filter?.lifecycle?.map(l => `lifecycle=${encodeURIComponent(l)}`) ?? []; + const query = [...tags, ...lifecycles].join('&'); + + const response = await fetch(`${baseUrl}/tools?${query}`); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return response.json() as Promise; + } +} diff --git a/plugins/explore/src/api/index.ts b/plugins/explore/src/api/index.ts new file mode 100644 index 0000000000..387393f231 --- /dev/null +++ b/plugins/explore/src/api/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ExploreClient } from './ExploreClient'; +export { exploreApiRef } from './ExploreApi'; +export type { ExploreApi } from './ExploreApi'; diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index 399112fac6..62d197803f 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -21,8 +21,8 @@ import React from 'react'; import { DefaultExplorePage } from './DefaultExplorePage'; describe('', () => { - const catalogApi: jest.Mocked = { - addLocation: jest.fn(_a => new Promise(() => {})), + const catalogApi = { + addLocation: jest.fn(), getEntities: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index c84363f5c6..a62083f66f 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -22,8 +22,8 @@ import React from 'react'; import { DomainExplorerContent } from './DomainExplorerContent'; describe('', () => { - const catalogApi: jest.Mocked = { - addLocation: jest.fn(_a => new Promise(() => {})), + const catalogApi = { + addLocation: jest.fn(), getEntities: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx index 23553b8b76..674ef1e43f 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.tsx @@ -43,8 +43,10 @@ import useAsync from 'react-use/lib/useAsync'; const useStyles = makeStyles((theme: BackstageTheme) => ({ graph: { - flex: 1, - minHeight: 0, + minHeight: '100%', + }, + graphWrapper: { + height: '100%', }, organizationNode: { fill: theme.palette.secondary.light, @@ -62,6 +64,15 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({ justifyContent: 'center', color: 'black', }, + legend: { + position: 'absolute', + bottom: 0, + right: 0, + padding: theme.spacing(1), + '& .icon': { + verticalAlign: 'bottom', + }, + }, textOrganization: { color: theme.palette.secondary.contrastText, }, @@ -221,7 +232,7 @@ export function GroupsDiagram() { } return ( - <> +
+ - Use pinch & zoom - to move around the diagram. + Use pinch & zoom to move around the + diagram. - +
); } diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index a4769e8f4f..9b461fba7d 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -22,8 +22,8 @@ import React from 'react'; import { GroupsExplorerContent } from '../GroupsExplorerContent'; describe('', () => { - const catalogApi: jest.Mocked = { - addLocation: jest.fn(_a => new Promise(() => {})), + const catalogApi = { + addLocation: jest.fn(), getEntities: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx index da35c9fa39..f3cca5642f 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx @@ -14,25 +14,23 @@ * limitations under the License. */ -import { - ExploreTool, - exploreToolsConfigRef, -} from '@backstage/plugin-explore-react'; +import { ExploreTool } from '@backstage/plugin-explore-common'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { waitFor } from '@testing-library/react'; import React from 'react'; +import { exploreApiRef } from '../../api'; import { ToolExplorerContent } from './ToolExplorerContent'; describe('', () => { - const exploreToolsConfigApi: jest.Mocked = { + const exploreApi: jest.Mocked = { getTools: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} @@ -63,7 +61,7 @@ describe('', () => { tags: ['standards', 'landscape'], }, ]; - exploreToolsConfigApi.getTools.mockResolvedValue(tools); + exploreApi.getTools.mockResolvedValue({ tools }); const { getByText } = await renderInTestApp( @@ -78,7 +76,7 @@ describe('', () => { }); it('renders a custom title', async () => { - exploreToolsConfigApi.getTools.mockResolvedValue([]); + exploreApi.getTools.mockResolvedValue({ tools: [] }); const { getByText } = await renderInTestApp( @@ -90,7 +88,7 @@ describe('', () => { }); it('renders empty state', async () => { - exploreToolsConfigApi.getTools.mockResolvedValue([]); + exploreApi.getTools.mockResolvedValue({ tools: [] }); const { getByText } = await renderInTestApp( diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx index 5404c67393..d9cd3a512c 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { exploreToolsConfigRef } from '@backstage/plugin-explore-react'; import React from 'react'; import useAsync from 'react-use/lib/useAsync'; import { ToolCard } from '../ToolCard'; @@ -28,16 +27,18 @@ import { WarningPanel, } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +import { exploreApiRef } from '../../api'; const Body = () => { - const exploreToolsConfigApi = useApi(exploreToolsConfigRef); + const exploreApi = useApi(exploreApiRef); + const { value: tools, loading, error, } = useAsync(async () => { - return await exploreToolsConfigApi.getTools(); - }, [exploreToolsConfigApi]); + return (await exploreApi.getTools())?.tools; + }, [exploreApi]); if (loading) { return ; diff --git a/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx b/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx new file mode 100644 index 0000000000..3f5a515efb --- /dev/null +++ b/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx @@ -0,0 +1,115 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactNode } from 'react'; +import { + Box, + Chip, + Divider, + ListItem, + ListItemIcon, + ListItemText, + makeStyles, +} from '@material-ui/core'; +import { Link } from '@backstage/core-components'; +import { useAnalytics } from '@backstage/core-plugin-api'; +import { + IndexableDocument, + ResultHighlight, +} from '@backstage/plugin-search-common'; +import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; + +const useStyles = makeStyles({ + flexContainer: { + flexWrap: 'wrap', + }, + itemText: { + width: '100%', + wordBreak: 'break-all', + marginBottom: '1rem', + }, +}); + +/** + * Props for {@link ToolSearchResultListItem}. + * + * @public + */ +export interface ToolSearchResultListItemProps { + icon?: ReactNode; + result: IndexableDocument; + highlight?: ResultHighlight; + rank?: number; +} + +/** @public */ +export function ToolSearchResultListItem(props: ToolSearchResultListItemProps) { + const result = props.result as any; + + const classes = useStyles(); + const analytics = useAnalytics(); + const handleClick = () => { + analytics.captureEvent('discover', result.title, { + attributes: { to: result.location }, + value: props.rank, + }); + }; + + return ( + <> + + {props.icon && {props.icon}} +
+ + {props.highlight?.fields.title ? ( + + ) : ( + result.title + )} + + } + secondary={ + props.highlight?.fields.text ? ( + + ) : ( + result.text + ) + } + /> + + {result.tags && + result.tags.map((tag: string) => ( + + ))} + +
+
+ + + ); +} diff --git a/plugins/explore/src/components/ToolSearchResultListItem/index.ts b/plugins/explore/src/components/ToolSearchResultListItem/index.ts new file mode 100644 index 0000000000..1dfdd64b5b --- /dev/null +++ b/plugins/explore/src/components/ToolSearchResultListItem/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './ToolSearchResultListItem'; diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts index f8fba16910..d3af1b99c2 100644 --- a/plugins/explore/src/components/index.ts +++ b/plugins/explore/src/components/index.ts @@ -16,3 +16,4 @@ export * from './DomainCard'; export * from './ExploreLayout'; +export * from './ToolSearchResultListItem'; diff --git a/plugins/explore/src/index.ts b/plugins/explore/src/index.ts index 04f0240968..24591211f7 100644 --- a/plugins/explore/src/index.ts +++ b/plugins/explore/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export * from './api'; export * from './components'; export * from './extensions'; export { explorePlugin, explorePlugin as plugin } from './plugin'; diff --git a/plugins/explore/src/plugin.ts b/plugins/explore/src/plugin.ts index a4bb538265..ab8eedf7ef 100644 --- a/plugins/explore/src/plugin.ts +++ b/plugins/explore/src/plugin.ts @@ -16,21 +16,44 @@ import { exploreToolsConfigRef } from '@backstage/plugin-explore-react'; import { catalogEntityRouteRef, exploreRouteRef } from './routes'; -import { exampleTools } from './util/examples'; -import { createApiFactory, createPlugin } from '@backstage/core-plugin-api'; +import { + createApiFactory, + createPlugin, + discoveryApiRef, + fetchApiRef, +} from '@backstage/core-plugin-api'; +import { ExploreClient, exploreApiRef } from './api'; +// import { exampleTools } from './util/examples'; /** @public */ export const explorePlugin = createPlugin({ id: 'explore', apis: [ - // Register a default for exploreToolsConfigRef, you may want to override - // the API locally in your app. + createApiFactory({ + api: exploreApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + exploreToolsConfig: exploreToolsConfigRef, + }, + factory: ({ discoveryApi, fetchApi, exploreToolsConfig }) => + // NOTE: The exploreToolsConfig is for backwards compatibility and will be removed in the future + new ExploreClient({ discoveryApi, fetchApi, exploreToolsConfig }), + }), + /** + * @deprecated Use ExploreApi from `@backstage/plugin-explore` instead + * + * Register a default for exploreToolsConfigRef, you may want to override + * the API locally in your app. + */ createApiFactory({ api: exploreToolsConfigRef, deps: {}, factory: () => ({ async getTools() { - return exampleTools; + // Returning `undefined` will enable the explore-backend to be used via the ExploreClient. + // If this API has been customized and returns data it will be respected first. + return undefined as any; }, }), }), diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index cd01270cbe..7457ed8f9c 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-firehydrant +## 0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/theme@0.2.16 + +## 0.1.29-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/theme@0.2.16 + +## 0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## 0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.28-next.0 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 2c455fbd82..8bbc8aaf5c 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-firehydrant", "description": "A Backstage plugin that integrates towards FireHydrant", - "version": "0.1.28-next.0", + "version": "0.1.29-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -46,7 +46,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index bd43f4d7b8..20ad50f001 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-fossa +## 0.2.44-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.2.44-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.2.43 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.2.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.2.43-next.0 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 32d9da5146..9dc277525b 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-fossa", "description": "A Backstage plugin that integrates towards FOSSA", - "version": "0.2.43-next.0", + "version": "0.2.44-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -59,7 +59,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx index 676a3cff3c..be07b4707f 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -15,21 +15,16 @@ */ import { Entity } from '@backstage/catalog-model'; -import { - CatalogApi, - catalogApiRef, - entityRouteRef, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { FossaApi, fossaApiRef } from '../../api'; import { FossaPage } from './FossaPage'; describe('', () => { - const catalogApi: jest.Mocked = { + const catalogApi = { addLocation: jest.fn(), getEntities: jest.fn(), - getEntityByRef: jest.fn(), getLocationByRef: jest.fn(), getLocationById: jest.fn(), removeEntityByUid: jest.fn(), diff --git a/plugins/gcalendar/CHANGELOG.md b/plugins/gcalendar/CHANGELOG.md index 3e4cc2473c..226a8ba905 100644 --- a/plugins/gcalendar/CHANGELOG.md +++ b/plugins/gcalendar/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-gcalendar +## 0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.3.8-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.3.7 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/gcalendar/package.json b/plugins/gcalendar/package.json index c6f748c6a2..07e90edbfc 100644 --- a/plugins/gcalendar/package.json +++ b/plugins/gcalendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcalendar", - "version": "0.3.7-next.0", + "version": "0.3.8-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -54,7 +54,7 @@ "@types/node": "^16.11.26", "@types/sanitize-html": "^2.6.2", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/gcalendar/src/components/CalendarCard/AttendeeChip.test.tsx b/plugins/gcalendar/src/components/CalendarCard/AttendeeChip.test.tsx index 64485e74ab..769a225de3 100644 --- a/plugins/gcalendar/src/components/CalendarCard/AttendeeChip.test.tsx +++ b/plugins/gcalendar/src/components/CalendarCard/AttendeeChip.test.tsx @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { screen } from '@testing-library/react'; import React from 'react'; - import { renderInTestApp } from '@backstage/test-utils'; - import { AttendeeChip } from './AttendeeChip'; import { EventAttendee, ResponseStatus } from '../../api'; @@ -27,8 +27,8 @@ describe('', () => { email, responseStatus: ResponseStatus.needsAction, }; - const { queryByText } = await renderInTestApp(); - expect(queryByText(email)).toBeInTheDocument(); + await renderInTestApp(); + expect(screen.getByText(email)).toBeInTheDocument(); }); it('renders accepted icon', async () => { @@ -37,8 +37,8 @@ describe('', () => { email, responseStatus: ResponseStatus.accepted, }; - const { getByTestId } = await renderInTestApp(); - expect(getByTestId('accepted-icon')).toBeInTheDocument(); + await renderInTestApp(); + expect(screen.getByTestId('accepted-icon')).toBeInTheDocument(); }); it('renders declined icon', async () => { @@ -47,7 +47,7 @@ describe('', () => { email, responseStatus: ResponseStatus.declined, }; - const { getByTestId } = await renderInTestApp(); - expect(getByTestId('declined-icon')).toBeInTheDocument(); + await renderInTestApp(); + expect(screen.getByTestId('declined-icon')).toBeInTheDocument(); }); }); diff --git a/plugins/gcalendar/src/components/CalendarCard/CalendarEvent.test.tsx b/plugins/gcalendar/src/components/CalendarCard/CalendarEvent.test.tsx index 7a48903bec..4a1ca884a9 100644 --- a/plugins/gcalendar/src/components/CalendarCard/CalendarEvent.test.tsx +++ b/plugins/gcalendar/src/components/CalendarCard/CalendarEvent.test.tsx @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { fireEvent } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import React from 'react'; - import { renderInTestApp } from '@backstage/test-utils'; - import { CalendarEvent } from './CalendarEvent'; describe('', () => { @@ -42,16 +42,14 @@ describe('', () => { }; it('should render calendar event', async () => { - const { queryByText, queryByTestId } = await renderInTestApp( - , - ); - expect(queryByText(event.summary)).toBeInTheDocument(); - expect(queryByTestId('calendar-event-zoom-link')).toBeInTheDocument(); - expect(queryByTestId('calendar-event-zoom-link')).toHaveAttribute( + await renderInTestApp(); + expect(screen.getByText(event.summary)).toBeInTheDocument(); + expect(screen.getByTestId('calendar-event-zoom-link')).toBeInTheDocument(); + expect(screen.queryByTestId('calendar-event-zoom-link')).toHaveAttribute( 'href', event.conferenceData.entryPoints[0].uri, ); - expect(queryByTestId('calendar-event-time')).toBeInTheDocument(); + expect(screen.getByTestId('calendar-event-time')).toBeInTheDocument(); }); it('should not render time for events longer than 1 day', async () => { @@ -64,20 +62,18 @@ describe('', () => { date: '2022-02-19', }, }; - const { queryByText, queryByTestId } = await renderInTestApp( - , - ); - expect(queryByText(allDayEvent.summary)).toBeInTheDocument(); - expect(queryByTestId('calendar-event-time')).not.toBeInTheDocument(); + await renderInTestApp(); + expect(screen.getByText(allDayEvent.summary)).toBeInTheDocument(); + expect(screen.queryByTestId('calendar-event-time')).not.toBeInTheDocument(); }); it('should show popover on click', async () => { - const { queryByTestId, getByTestId } = await renderInTestApp( - , - ); - expect(queryByTestId('calendar-event-popover')).not.toBeInTheDocument(); + await renderInTestApp(); + expect( + screen.queryByTestId('calendar-event-popover'), + ).not.toBeInTheDocument(); - fireEvent.click(getByTestId('calendar-event')); - expect(queryByTestId('calendar-event-popover')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('calendar-event')); + expect(screen.getByTestId('calendar-event-popover')).toBeInTheDocument(); }); }); diff --git a/plugins/gcalendar/src/components/CalendarCard/CalendarEventPopoverContent.test.tsx b/plugins/gcalendar/src/components/CalendarCard/CalendarEventPopoverContent.test.tsx index fe48b2fd5a..107a3ed180 100644 --- a/plugins/gcalendar/src/components/CalendarCard/CalendarEventPopoverContent.test.tsx +++ b/plugins/gcalendar/src/components/CalendarCard/CalendarEventPopoverContent.test.tsx @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; - +import { screen } from '@testing-library/react'; import { renderInTestApp } from '@backstage/test-utils'; - import { CalendarEventPopoverContent } from './CalendarEventPopoverContent'; describe('', () => { @@ -35,21 +35,18 @@ describe('', () => { }; it('should render event info', async () => { - const { queryByText, queryByTestId } = await renderInTestApp( - , - ); - expect(queryByText(event.summary)).toBeInTheDocument(); - expect(queryByText(event.description)).toBeInTheDocument(); - expect(queryByText(event.attendees[0].email)).toBeInTheDocument(); - expect(queryByText('Join Zoom Meeting')).toBeInTheDocument(); - expect(queryByText('Join Zoom Meeting')?.closest('a')).toHaveAttribute( - 'href', - event.conferenceData.entryPoints[0].uri, - ); - expect(queryByTestId('open-calendar-link')).toHaveAttribute( + await renderInTestApp(); + expect(screen.getByText(event.summary)).toBeInTheDocument(); + expect(screen.getByText(event.description)).toBeInTheDocument(); + expect(screen.getByText(event.attendees[0].email)).toBeInTheDocument(); + expect(screen.getByText('Join Zoom Meeting')).toBeInTheDocument(); + expect( + screen.queryByText('Join Zoom Meeting')?.closest('a'), + ).toHaveAttribute('href', event.conferenceData.entryPoints[0].uri); + expect(screen.queryByTestId('open-calendar-link')).toHaveAttribute( 'href', event.htmlLink, ); - expect(queryByText(event.attendees[0].email)).toBeInTheDocument(); + expect(screen.getByText(event.attendees[0].email)).toBeInTheDocument(); }); }); diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index 3e4f8df529..ed1ae0d467 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-gcp-projects +## 0.3.31-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## 0.3.31-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## 0.3.30 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## 0.3.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + ## 0.3.30-next.0 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 6b1ea7a0d4..e6e60fb9e2 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gcp-projects", "description": "A Backstage plugin that helps you manage projects in GCP", - "version": "0.3.30-next.0", + "version": "0.3.31-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,7 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@react-hookz/web": "^15.0.0" + "@react-hookz/web": "^20.0.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", @@ -55,7 +55,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx index b900b81695..364033ce73 100644 --- a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx +++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx @@ -61,7 +61,9 @@ const DetailsPage = () => { const classes = useStyles(); const [{ status, result: details, error }, { execute }] = useAsync(async () => - api.getProject(decodeURIComponent(location.search.split('projectId=')[1])), + api.getProject( + decodeURIComponent(window.location.search.split('projectId=')[1]), + ), ); useMountEffect(execute); diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index 00b079b7cd..e5e69c52ab 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-git-release-manager +## 0.3.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/theme@0.2.16 + +## 0.3.25-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/theme@0.2.16 + +## 0.3.24 + +### Patch Changes + +- 43afded227: Updated recharts to v2.0.0 and fixed typing issues +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/theme@0.2.16 + +## 0.3.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + ## 0.3.24-next.0 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 9b3533d994..1ba966c915 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.3.24-next.0", + "version": "0.3.25-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -53,7 +53,7 @@ "@types/node": "^16.11.26", "@types/recharts": "^1.8.15", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/git-release-manager/src/features/Features.test.tsx b/plugins/git-release-manager/src/features/Features.test.tsx index a77dcfe9fc..f3a70c2bda 100644 --- a/plugins/git-release-manager/src/features/Features.test.tsx +++ b/plugins/git-release-manager/src/features/Features.test.tsx @@ -55,7 +55,7 @@ describe('Features', () => { expect(getByTestId(TEST_IDS.info.info)).toMatchInlineSnapshot(`
{ : The source control system where releases reside in a practical sense. Read more about { > Git releases , Opens in a new window diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index b3570d20cf..e6bcf0b12c 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-github-actions +## 0.5.12-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.5.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.5.11 + +### Patch Changes + +- ed438a3ba5: Add error panel when the plugin fails. +- 0d6837ca4e: Fix wrong GitHub callback URL documentation +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.5.11-next.1 + +### Patch Changes + +- ed438a3ba5: Add error panel when the plugin fails. +- 0d6837ca4e: Fix wrong GitHub callback URL documentation +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.5.11-next.0 ### Patch Changes diff --git a/plugins/github-actions/README.md b/plugins/github-actions/README.md index 792d13280f..cb6867193c 100644 --- a/plugins/github-actions/README.md +++ b/plugins/github-actions/README.md @@ -11,7 +11,7 @@ TBD ### Generic Requirements 1. Provide OAuth credentials: - 1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) in the GitHub organization with the callback URL set to `http://localhost:7007/api/auth/github`. + 1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) in the GitHub organization with the callback URL set to `http://localhost:7007/api/auth/github/handler/frame`. 2. Take the Client ID and Client Secret from the newly created app's settings page and put them into `AUTH_GITHUB_CLIENT_ID` and `AUTH_GITHUB_CLIENT_SECRET` environment variables. 2. Annotate your component with a correct GitHub Actions repository and owner: diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 8247c48fa5..9f03e38b4b 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.5.11-next.0", + "version": "0.5.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -62,7 +62,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx index 89fd40da33..59d64af9c9 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -29,6 +29,7 @@ import { useRouteRef, } from '@backstage/core-plugin-api'; import { + ErrorPanel, InfoCard, InfoCardVariants, Link, @@ -76,6 +77,11 @@ export const RecentWorkflowRunsCard = (props: { const githubHost = hostname || 'github.com'; const routeLink = useRouteRef(buildRouteRef); + + if (error) { + return ; + } + return ( } - aria-controls={`panel-${name}-content`} - id={`panel-${name}-header`} IconButtonProps={{ className: classes.button, }} diff --git a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx index 075b73dd98..2c9d41398a 100644 --- a/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx +++ b/plugins/github-actions/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx @@ -105,8 +105,6 @@ export const WorkflowRunLogs = ({ } - aria-controls={`panel-${name}-content`} - id={`panel-${name}-header`} IconButtonProps={{ className: classes.button, }} diff --git a/plugins/github-actions/src/components/useWorkflowRuns.ts b/plugins/github-actions/src/components/useWorkflowRuns.ts index e78e3fbb3f..9ffbdad452 100644 --- a/plugins/github-actions/src/components/useWorkflowRuns.ts +++ b/plugins/github-actions/src/components/useWorkflowRuns.ts @@ -87,7 +87,9 @@ export function useWorkflowRuns({ runId: run.id, }); } catch (e) { - errorApi.post(e); + errorApi.post( + new Error(`Failed to rerun the workflow: ${e.message}`), + ); } }, source: { diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index 0a9a4215df..f670411444 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-github-deployments +## 0.1.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.43-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.42 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/integration-react@1.1.6 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.1.42-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.42-next.0 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index a25fab05db..85911ffadc 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-deployments", "description": "A Backstage plugin that integrates towards GitHub Deployments", - "version": "0.1.42-next.0", + "version": "0.1.43-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/github-issues/CHANGELOG.md b/plugins/github-issues/CHANGELOG.md index a812495a5f..0bbb84d63a 100644 --- a/plugins/github-issues/CHANGELOG.md +++ b/plugins/github-issues/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-github-issues +## 0.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.2.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.2.0 + +### Minor Changes + +- ead285b9e4: **BREAKING**: Changed the casing of all exported types to have a lowercase "h" in "github". E.g. "GitHubIssuesPage" was renamed to "GithubIssuesPage". Please rename your imports where necessary. + +### Patch Changes + +- c8dd2a8c87: Stripping specific issues URL already present to target base issues URL. +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.2.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.2.0-next.0 ### Minor Changes diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json index 3853e845a6..40e0c1b878 100644 --- a/plugins/github-issues/package.json +++ b/plugins/github-issues/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-issues", - "version": "0.2.0-next.0", + "version": "0.2.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,7 @@ "@types/node": "*", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/github-pull-requests-board/CHANGELOG.md b/plugins/github-pull-requests-board/CHANGELOG.md index a91ceae5f0..e557f4b05f 100644 --- a/plugins/github-pull-requests-board/CHANGELOG.md +++ b/plugins/github-pull-requests-board/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-github-pull-requests-board +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.6-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.5 + +### Patch Changes + +- cc8bfc56c3: Add a new "Team" Filter Options to the Github Pull Requests Dashboard. + + When toggling this option on, the dashboard will displays all of the PRs opened + by the members of that team on any repositories of the organization. + +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.5-next.0 ### Patch Changes diff --git a/plugins/github-pull-requests-board/package.json b/plugins/github-pull-requests-board/package.json index c6f58ebf0f..d8dc59e7b6 100644 --- a/plugins/github-pull-requests-board/package.json +++ b/plugins/github-pull-requests-board/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-pull-requests-board", "description": "A Backstage plugin that allows you to see all open Pull Requests for all the repositories owned by your team", - "version": "0.1.5-next.0", + "version": "0.1.6-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -58,7 +58,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index a578fe2077..9735fbdb7e 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-gitops-profiles +## 0.3.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## 0.3.30-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## 0.3.29 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## 0.3.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + ## 0.3.29-next.0 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 000f1c4b6c..d6a8c8c201 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gitops-profiles", "description": "A Backstage plugin that helps you manage GitOps profiles", - "version": "0.3.29-next.0", + "version": "0.3.30-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -55,7 +55,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md index 4409bc5861..f76b3a28b2 100644 --- a/plugins/gocd/CHANGELOG.md +++ b/plugins/gocd/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-gocd +## 0.1.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.18-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.17-next.0 ### Patch Changes diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json index e8ba4b9374..a247804a73 100644 --- a/plugins/gocd/package.json +++ b/plugins/gocd/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gocd", "description": "A Backstage plugin that integrates towards GoCD", - "version": "0.1.17-next.0", + "version": "0.1.18-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -58,7 +58,7 @@ "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/gocd/src/components/Select/Select.test.tsx b/plugins/gocd/src/components/Select/Select.test.tsx index fb467b946f..9d73194ed8 100644 --- a/plugins/gocd/src/components/Select/Select.test.tsx +++ b/plugins/gocd/src/components/Select/Select.test.tsx @@ -40,7 +40,7 @@ describe('Select', () => { expect(rendered.getAllByText(testLabel)).toHaveLength(2); }); - describe('when the user hasn`t clicked on it', () => { + describe("when the user hasn't clicked on it", () => { it('should only render the current select item', async () => { const rendered = await renderInTestApp( { - await import('promise-any-polyfill'); - })(); -} - export class JenkinsApiImpl { private static readonly lastBuildTreeSpec = `lastBuild[ number, diff --git a/plugins/jenkins-common/CHANGELOG.md b/plugins/jenkins-common/CHANGELOG.md index fc9c74184d..e45d9ae009 100644 --- a/plugins/jenkins-common/CHANGELOG.md +++ b/plugins/jenkins-common/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-jenkins-common +## 0.1.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## 0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.1 + - @backstage/plugin-catalog-common@1.0.8 + ## 0.1.10-next.0 ### Patch Changes diff --git a/plugins/jenkins-common/package.json b/plugins/jenkins-common/package.json index 95158ea8bb..9c898ea53a 100644 --- a/plugins/jenkins-common/package.json +++ b/plugins/jenkins-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins-common", - "version": "0.1.10-next.0", + "version": "0.1.11-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 2a15d2917a..7aefb32e40 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-jenkins +## 0.7.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-jenkins-common@0.1.11-next.1 + +## 0.7.11-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-jenkins-common@0.1.11-next.0 + +## 0.7.10 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-jenkins-common@0.1.10 + +## 0.7.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-jenkins-common@0.1.10-next.0 + ## 0.7.10-next.0 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index a77f3d2439..1c9a88e045 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.7.10-next.0", + "version": "0.7.11-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -62,7 +62,7 @@ "@types/node": "^16.11.26", "@types/testing-library__jest-dom": "^5.9.1", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index 5999228785..6b9be8a94c 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-kafka-backend +## 0.2.32-next.1 + +### Patch Changes + +- 0b54dbaf7d: Internal tweak to adapt to `kafkajs` 2.2.3 +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.2.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.2.31 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.2.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + ## 0.2.31-next.0 ### Patch Changes diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 096170dc7e..93b662cf50 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka-backend", "description": "A Backstage backend plugin that integrates towards Kafka", - "version": "0.2.31-next.0", + "version": "0.2.32-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kafka-backend/src/types/types.ts b/plugins/kafka-backend/src/types/types.ts index 1fb9701334..f5ffd6b6c7 100644 --- a/plugins/kafka-backend/src/types/types.ts +++ b/plugins/kafka-backend/src/types/types.ts @@ -25,8 +25,19 @@ export interface ClusterDetails { export type SslConfig = ConnectionOptions | boolean; -export type SaslConfig = { - mechanism: 'plain' | 'scram-sha-256' | 'scram-sha-512'; - username: string; - password: string; -}; +export type SaslConfig = + | { + mechanism: 'plain'; + username: string; + password: string; + } + | { + mechanism: 'scram-sha-256'; + username: string; + password: string; + } + | { + mechanism: 'scram-sha-512'; + username: string; + password: string; + }; diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index ec8563d64b..a2242f66e7 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-kafka +## 0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.3.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## 0.3.11 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## 0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 0bcb7dfa24..8d327a6451 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.3.11-next.0", + "version": "0.3.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,7 @@ "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", "jest-when": "^3.1.0", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 804112324d..bc548e9190 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-kubernetes-backend +## 0.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-test-utils@0.1.31-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-kubernetes-common@0.4.5-next.1 + +## 0.8.1-next.0 + +### Patch Changes + +- b585179770: Added Kubernetes proxy API route to backend Kubernetes plugin, allowing Backstage plugin developers to read/write new information from Kubernetes (if proper credentials are provided). +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-test-utils@0.1.31-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-kubernetes-common@0.4.5-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.8.0 + +### Minor Changes + +- cbf5d11fdf: The Kubernetes errors when fetching pod metrics are now captured and returned to the frontend. + + - **BREAKING** The method `fetchPodMetricsByNamespace` in the interface `KubernetesFetcher` is changed to `fetchPodMetricsByNamespaces`. It now accepts a set of namespace strings and returns `Promise`. + - Add the `PodStatusFetchResponse` to the `FetchResponse` union type. + - Add `NOT_FOUND` to the `KubernetesErrorTypes` union type, the HTTP error with status code 404 will be mapped to this error. + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-kubernetes-common@0.4.4 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.8.0-next.1 + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-kubernetes-common@0.4.4-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + ## 0.8.0-next.0 ### Minor Changes diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index d51ec4c211..b88b3f2a02 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -22,6 +22,7 @@ import { Logger } from 'winston'; import { Metrics } from '@kubernetes/client-node'; import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import type { RequestHandler } from 'express'; import { TokenCredential } from '@azure/identity'; // @alpha (undocumented) @@ -142,6 +143,9 @@ export class GoogleServiceAccountAuthTranslator ): Promise; } +// @alpha +export const HEADER_KUBERNETES_CLUSTER: string; + // @alpha (undocumented) export interface KubernetesAuthTranslator { // (undocumented) @@ -188,10 +192,16 @@ export class KubernetesBuilder { options: KubernetesObjectsProviderOptions, ): KubernetesObjectsProvider; // (undocumented) + protected buildProxy( + logger: Logger, + clusterSupplier: KubernetesClustersSupplier, + ): KubernetesProxy; + // (undocumented) protected buildRouter( objectsProvider: KubernetesObjectsProvider, clusterSupplier: KubernetesClustersSupplier, catalogApi: CatalogApi, + proxy: KubernetesProxy, ): express.Router; // (undocumented) protected buildServiceLocator( @@ -207,8 +217,23 @@ export class KubernetesBuilder { clusterSupplier: KubernetesClustersSupplier, ): Promise; // (undocumented) + protected getClusterSupplier(): KubernetesClustersSupplier; + // (undocumented) + protected getFetcher(): KubernetesFetcher; + // (undocumented) + protected getObjectsProvider( + options: KubernetesObjectsProviderOptions, + ): KubernetesObjectsProvider; + // (undocumented) protected getObjectTypesToFetch(): ObjectToFetch[] | undefined; // (undocumented) + protected getProxy( + logger: Logger, + clusterSupplier: KubernetesClustersSupplier, + ): KubernetesProxy; + // (undocumented) + protected getServiceLocator(): KubernetesServiceLocator; + // (undocumented) protected getServiceLocatorMethod(): ServiceLocatorMethod; // (undocumented) setClusterSupplier(clusterSupplier?: KubernetesClustersSupplier): this; @@ -219,6 +244,8 @@ export class KubernetesBuilder { // (undocumented) setObjectsProvider(objectsProvider?: KubernetesObjectsProvider): this; // (undocumented) + setProxy(proxy?: KubernetesProxy): this; + // (undocumented) setServiceLocator(serviceLocator?: KubernetesServiceLocator): this; } @@ -228,6 +255,7 @@ export type KubernetesBuilderReturn = Promise<{ clusterSupplier: KubernetesClustersSupplier; customResources: CustomResource[]; fetcher: KubernetesFetcher; + proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider; serviceLocator: KubernetesServiceLocator; }>; @@ -322,6 +350,13 @@ export type KubernetesObjectTypes = | 'statefulsets' | 'daemonsets'; +// @alpha +export class KubernetesProxy { + constructor(logger: Logger, clusterSupplier: KubernetesClustersSupplier); + // (undocumented) + createRequestHandler(): RequestHandler; +} + // @alpha export interface KubernetesServiceLocator { // (undocumented) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 4082bd5b3a..100b67839f 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.8.0-next.0", + "version": "0.8.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,6 +36,7 @@ "dependencies": { "@azure/identity": "^2.0.4", "@backstage/backend-common": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", @@ -43,7 +44,8 @@ "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@google-cloud/container": "^4.0.0", - "@kubernetes/client-node": "^0.17.0", + "@jest-mock/express": "^2.0.1", + "@kubernetes/client-node": "0.17.0", "@types/express": "^4.17.6", "@types/luxon": "^3.0.0", "aws-sdk": "^2.840.0", @@ -54,9 +56,11 @@ "express-promise-router": "^4.1.0", "fs-extra": "10.1.0", "helmet": "^6.0.0", + "http-proxy-middleware": "^2.0.6", "lodash": "^4.17.21", "luxon": "^3.0.0", "morgan": "^1.10.0", + "node-fetch": "^2.6.7", "stream-buffers": "^3.0.2", "winston": "^3.2.1", "yn": "^4.0.0" @@ -64,7 +68,9 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/aws4": "^1.5.1", + "@types/http-proxy-middleware": "^0.19.3", "aws-sdk-mock": "^5.2.1", + "msw": "^0.49.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/kubernetes-backend/src/index.ts b/plugins/kubernetes-backend/src/index.ts index e2eee3f24e..67aefbd974 100644 --- a/plugins/kubernetes-backend/src/index.ts +++ b/plugins/kubernetes-backend/src/index.ts @@ -20,19 +20,6 @@ * @packageDocumentation */ -export * from './kubernetes-auth-translator/AwsIamKubernetesAuthTranslator'; -export * from './kubernetes-auth-translator/AzureIdentityKubernetesAuthTranslator'; -export * from './kubernetes-auth-translator/GoogleKubernetesAuthTranslator'; -export * from './kubernetes-auth-translator/GoogleServiceAccountAuthProvider'; -export * from './kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; -export * from './kubernetes-auth-translator/NoopKubernetesAuthTranslator'; -export * from './kubernetes-auth-translator/OidcKubernetesAuthTranslator'; -export * from './kubernetes-auth-translator/types'; - -export * from './service/router'; -export * from './service/KubernetesBuilder'; -export * from './service/KubernetesClientProvider'; - -export * from './types/types'; - -export { DEFAULT_OBJECTS } from './service/KubernetesFanOutHandler'; +export * from './kubernetes-auth-translator'; +export * from './service'; +export * from './types'; diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts new file mode 100644 index 0000000000..19f6dd80eb --- /dev/null +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './AwsIamKubernetesAuthTranslator'; +export * from './AzureIdentityKubernetesAuthTranslator'; +export * from './GoogleKubernetesAuthTranslator'; +export * from './GoogleServiceAccountAuthProvider'; +export * from './KubernetesAuthTranslatorGenerator'; +export * from './NoopKubernetesAuthTranslator'; +export * from './OidcKubernetesAuthTranslator'; +export * from './types'; diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 304885f3e2..c8ff98f592 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -13,23 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import express from 'express'; import Router from 'express-promise-router'; -import { Logger } from 'winston'; import { Duration } from 'luxon'; +import { Logger } from 'winston'; + import { getCombinedClusterSupplier } from '../cluster-locator'; +import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; import { - KubernetesObjectTypes, - ServiceLocatorMethod, CustomResource, - KubernetesObjectsProvider, - ObjectsByEntityRequest, KubernetesClustersSupplier, KubernetesFetcher, - KubernetesServiceLocator, + KubernetesObjectsProvider, KubernetesObjectsProviderOptions, + KubernetesObjectTypes, + KubernetesServiceLocator, + ObjectsByEntityRequest, + ServiceLocatorMethod, } from '../types/types'; import { KubernetesClientProvider } from './KubernetesClientProvider'; import { @@ -37,8 +40,7 @@ import { KubernetesFanOutHandler, } from './KubernetesFanOutHandler'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; -import { addResourceRoutesToRouter } from '../routes/resourcesRoutes'; -import { CatalogApi } from '@backstage/catalog-client'; +import { KubernetesProxy } from './KubernetesProxy'; /** * @@ -60,6 +62,7 @@ export type KubernetesBuilderReturn = Promise<{ clusterSupplier: KubernetesClustersSupplier; customResources: CustomResource[]; fetcher: KubernetesFetcher; + proxy: KubernetesProxy; objectsProvider: KubernetesObjectsProvider; serviceLocator: KubernetesServiceLocator; }>; @@ -76,6 +79,7 @@ export class KubernetesBuilder { private objectsProvider?: KubernetesObjectsProvider; private fetcher?: KubernetesFetcher; private serviceLocator?: KubernetesServiceLocator; + private proxy?: KubernetesProxy; static createBuilder(env: KubernetesEnvironment) { return new KubernetesBuilder(env); @@ -102,36 +106,34 @@ export class KubernetesBuilder { } const customResources = this.buildCustomResources(); - const fetcher = this.fetcher ?? this.buildFetcher(); + const fetcher = this.getFetcher(); - const clusterSupplier = - this.clusterSupplier ?? - this.buildClusterSupplier(this.defaultClusterRefreshInterval); + const clusterSupplier = this.getClusterSupplier(); - const serviceLocator = - this.serviceLocator ?? - this.buildServiceLocator(this.getServiceLocatorMethod(), clusterSupplier); + const proxy = this.getProxy(logger, clusterSupplier); - const objectsProvider = - this.objectsProvider ?? - this.buildObjectsProvider({ - logger, - fetcher, - serviceLocator, - customResources, - objectTypesToFetch: this.getObjectTypesToFetch(), - }); + const serviceLocator = this.getServiceLocator(); + + const objectsProvider = this.getObjectsProvider({ + logger, + fetcher, + serviceLocator, + customResources, + objectTypesToFetch: this.getObjectTypesToFetch(), + }); const router = this.buildRouter( objectsProvider, clusterSupplier, this.env.catalogApi, + proxy, ); return { clusterSupplier, customResources, fetcher, + proxy, objectsProvider, router, serviceLocator, @@ -163,6 +165,11 @@ export class KubernetesBuilder { return this; } + public setProxy(proxy?: KubernetesProxy) { + this.proxy = proxy; + return this; + } + protected buildCustomResources() { const customResources: CustomResource[] = ( this.env.config.getOptionalConfigArray('kubernetes.customResources') ?? [] @@ -186,24 +193,29 @@ export class KubernetesBuilder { refreshInterval: Duration, ): KubernetesClustersSupplier { const config = this.env.config; - return getCombinedClusterSupplier( + this.clusterSupplier = getCombinedClusterSupplier( config, this.env.catalogApi, refreshInterval, ); + + return this.clusterSupplier; } protected buildObjectsProvider( options: KubernetesObjectsProviderOptions, ): KubernetesObjectsProvider { - return new KubernetesFanOutHandler(options); + this.objectsProvider = new KubernetesFanOutHandler(options); + return this.objectsProvider; } protected buildFetcher(): KubernetesFetcher { - return new KubernetesClientBasedFetcher({ + this.fetcher = new KubernetesClientBasedFetcher({ kubernetesClientProvider: new KubernetesClientProvider(), logger: this.env.logger, }); + + return this.fetcher; } protected buildServiceLocator( @@ -212,14 +224,19 @@ export class KubernetesBuilder { ): KubernetesServiceLocator { switch (method) { case 'multiTenant': - return this.buildMultiTenantServiceLocator(clusterSupplier); + this.serviceLocator = + this.buildMultiTenantServiceLocator(clusterSupplier); + break; case 'http': - return this.buildHttpServiceLocator(clusterSupplier); + this.serviceLocator = this.buildHttpServiceLocator(clusterSupplier); + break; default: throw new Error( `Unsupported kubernetes.clusterLocatorMethod "${method}"`, ); } + + return this.serviceLocator; } protected buildMultiTenantServiceLocator( @@ -234,10 +251,19 @@ export class KubernetesBuilder { throw new Error('not implemented'); } + protected buildProxy( + logger: Logger, + clusterSupplier: KubernetesClustersSupplier, + ): KubernetesProxy { + this.proxy = new KubernetesProxy(logger, clusterSupplier); + return this.proxy; + } + protected buildRouter( objectsProvider: KubernetesObjectsProvider, clusterSupplier: KubernetesClustersSupplier, catalogApi: CatalogApi, + proxy: KubernetesProxy, ): express.Router { const logger = this.env.logger; const router = Router(); @@ -273,6 +299,8 @@ export class KubernetesBuilder { }); }); + router.use('/proxy', proxy.createRequestHandler()); + addResourceRoutesToRouter(router, catalogApi, objectsProvider); return router; @@ -296,6 +324,31 @@ export class KubernetesBuilder { ) as ServiceLocatorMethod; } + protected getFetcher(): KubernetesFetcher { + return this.fetcher ?? this.buildFetcher(); + } + + protected getClusterSupplier() { + return ( + this.clusterSupplier ?? + this.buildClusterSupplier(this.defaultClusterRefreshInterval) + ); + } + + protected getServiceLocator(): KubernetesServiceLocator { + return ( + this.serviceLocator ?? + this.buildServiceLocator( + this.getServiceLocatorMethod(), + this.getClusterSupplier(), + ) + ); + } + + protected getObjectsProvider(options: KubernetesObjectsProviderOptions) { + return this.objectsProvider ?? this.buildObjectsProvider(options); + } + protected getObjectTypesToFetch() { const objectTypesToFetchStrings = this.env.config.getOptionalStringArray( 'kubernetes.objectTypes', @@ -325,4 +378,11 @@ export class KubernetesBuilder { return objectTypesToFetch; } + + protected getProxy( + logger: Logger, + clusterSupplier: KubernetesClustersSupplier, + ) { + return this.proxy ?? this.buildProxy(logger, clusterSupplier); + } } diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts new file mode 100644 index 0000000000..4254bb33da --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -0,0 +1,119 @@ +/* + * 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 'buffer'; +import { getVoidLogger } from '@backstage/backend-common'; +import { NotFoundError } from '@backstage/errors'; +import { getMockReq, getMockRes } from '@jest-mock/express'; +import type { Request } from 'express'; +import express from 'express'; +import request from 'supertest'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; +import { + APPLICATION_JSON, + HEADER_KUBERNETES_CLUSTER, + KubernetesProxy, +} from './KubernetesProxy'; + +describe('KubernetesProxy', () => { + let proxy: KubernetesProxy; + const worker = setupServer(); + + setupRequestMockHandlers(worker); + + const buildMockRequest = (clusterName: any, path: string): Request => { + const req = getMockReq({ + params: { + path, + }, + header: jest.fn((key: string) => { + switch (key) { + case 'Content-Type': { + return APPLICATION_JSON; + } + case HEADER_KUBERNETES_CLUSTER: { + return clusterName; + } + default: { + return ''; + } + } + }), + }); + + return req; + }; + + const clusterSupplier: jest.Mocked = { + getClusters: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + proxy = new KubernetesProxy(getVoidLogger(), clusterSupplier); + }); + + it('should return a ERROR_NOT_FOUND if no clusters are found', async () => { + clusterSupplier.getClusters.mockResolvedValue([]); + + const req = buildMockRequest('test', 'api'); + const { res, next } = getMockRes(); + + await expect(proxy.createRequestHandler()(req, res, next)).rejects.toThrow( + NotFoundError, + ); + }); + + it('should pass the exact response from Kubernetes', async () => { + const apiResponse = { + kind: 'APIVersions', + versions: ['v1'], + serverAddressByClientCIDRs: [ + { + clientCIDR: '0.0.0.0/0', + serverAddress: '192.168.0.1:3333', + }, + ], + }; + + clusterSupplier.getClusters.mockResolvedValue([ + { + name: 'cluster1', + url: 'https://localhost:9999', + serviceAccountToken: '', + authProvider: 'serviceAccount', + }, + ] as ClusterDetails[]); + const app = express().use('/mountpath', proxy.createRequestHandler()); + const requestPromise = request(app) + .get('/mountpath/api') + .set(HEADER_KUBERNETES_CLUSTER, 'cluster1'); + worker.use( + rest.get('https://localhost:9999/api', (_, res, ctx) => + res(ctx.status(299), ctx.json(apiResponse)), + ), + rest.all(requestPromise.url, (req, _res, _ctx) => req.passthrough()), + ); + + const response = await requestPromise; + + expect(response.status).toEqual(299); + expect(response.body).toStrictEqual(apiResponse); + }); +}); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts new file mode 100644 index 0000000000..9d88d2ab84 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ErrorResponseBody, + ForwardedError, + InputError, + NotFoundError, + serializeError, +} from '@backstage/errors'; +import { bufferFromFileOrString } from '@kubernetes/client-node'; +import type { Request, RequestHandler } from 'express'; +import { createProxyMiddleware } from 'http-proxy-middleware'; +import { Logger } from 'winston'; +import { ClusterDetails, KubernetesClustersSupplier } from '../types/types'; + +export const APPLICATION_JSON: string = 'application/json'; + +/** + * The header that is used to specify the cluster name. + * + * @alpha + */ +export const HEADER_KUBERNETES_CLUSTER: string = 'X-Kubernetes-Cluster'; + +/** + * A proxy that routes requests to the Kubernetes API. + * + * @alpha + */ +export class KubernetesProxy { + private readonly middlewareForClusterName = new Map(); + + constructor( + private readonly logger: Logger, + private readonly clusterSupplier: KubernetesClustersSupplier, + ) {} + + public createRequestHandler(): RequestHandler { + return async (req, res, next) => { + const middleware = await this.getMiddleware(req); + middleware(req, res, next); + }; + } + + // We create one middleware per remote cluster and hold on to them, because + // the secure property isn't possible to decide on a per-request basis with a + // single middleware instance - and we don't expect it to change over time. + private async getMiddleware(originalReq: Request): Promise { + const originalCluster = await this.getClusterForRequest(originalReq); + let middleware = this.middlewareForClusterName.get(originalCluster.name); + if (!middleware) { + // Probably too risky without permissions protecting this endpoint + // if (cluster.serviceAccountToken) { + // options.headers = { + // Authorization: `Bearer ${cluster.serviceAccountToken}`, + // }; + // } + + const logger = this.logger.child({ cluster: originalCluster.name }); + middleware = createProxyMiddleware({ + logProvider: () => logger, + secure: !originalCluster.skipTLSVerify, + router: async req => { + // Re-evaluate the cluster on each request, in case it has changed + const cluster = await this.getClusterForRequest(req); + const url = new URL(cluster.url); + return { + protocol: url.protocol, + host: url.hostname, + port: url.port, + ca: bufferFromFileOrString('', cluster.caData)?.toString(), + }; + }, + pathRewrite: { [`^${originalReq.baseUrl}`]: '' }, + onError: (error, req, res) => { + const wrappedError = new ForwardedError( + `Cluster '${originalCluster.name}' request error`, + error, + ); + + logger.error(wrappedError); + + const body: ErrorResponseBody = { + error: serializeError(wrappedError, { + includeStack: process.env.NODE_ENV === 'development', + }), + request: { method: req.method, url: req.originalUrl }, + response: { statusCode: 500 }, + }; + + res.status(500).json(body); + }, + }); + + this.middlewareForClusterName.set(originalCluster.name, middleware); + } + + return middleware; + } + + private async getClusterForRequest(req: Request): Promise { + const clusterName = req.header(HEADER_KUBERNETES_CLUSTER); + if (!clusterName) { + throw new InputError(`Missing '${HEADER_KUBERNETES_CLUSTER}' header.`); + } + + const cluster = await this.clusterSupplier + .getClusters() + .then(clusters => clusters.find(c => c.name === clusterName)); + if (!cluster) { + throw new NotFoundError(`Cluster '${clusterName}' not found`); + } + + return cluster; + } +} diff --git a/plugins/kubernetes-backend/src/service/index.ts b/plugins/kubernetes-backend/src/service/index.ts new file mode 100644 index 0000000000..70ff530bee --- /dev/null +++ b/plugins/kubernetes-backend/src/service/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './KubernetesBuilder'; +export * from './KubernetesClientProvider'; +export { DEFAULT_OBJECTS } from './KubernetesFanOutHandler'; +export { HEADER_KUBERNETES_CLUSTER, KubernetesProxy } from './KubernetesProxy'; +export * from './router'; diff --git a/plugins/kubernetes-backend/src/types/index.ts b/plugins/kubernetes-backend/src/types/index.ts new file mode 100644 index 0000000000..db229eae34 --- /dev/null +++ b/plugins/kubernetes-backend/src/types/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index b85e3c00ee..38fa766fd3 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-kubernetes-common +## 0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.4-next.1 + +## 0.4.5-next.0 + +### Patch Changes + +- b585179770: Added Kubernetes proxy API route to backend Kubernetes plugin, allowing Backstage plugin developers to read/write new information from Kubernetes (if proper credentials are provided). +- Updated dependencies + - @backstage/catalog-model@1.1.4-next.0 + +## 0.4.4 + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- cbf5d11fdf: The Kubernetes errors when fetching pod metrics are now captured and returned to the frontend. + + - **BREAKING** The method `fetchPodMetricsByNamespace` in the interface `KubernetesFetcher` is changed to `fetchPodMetricsByNamespaces`. It now accepts a set of namespace strings and returns `Promise`. + - Add the `PodStatusFetchResponse` to the `FetchResponse` union type. + - Add `NOT_FOUND` to the `KubernetesErrorTypes` union type, the HTTP error with status code 404 will be mapped to this error. + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + +## 0.4.4-next.1 + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/catalog-model@1.1.3-next.0 + ## 0.4.4-next.0 ### Patch Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 48c04fb222..46a58c0cbc 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-common", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", - "version": "0.4.4-next.0", + "version": "0.4.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,7 +39,7 @@ }, "dependencies": { "@backstage/catalog-model": "workspace:^", - "@kubernetes/client-node": "^0.17.0" + "@kubernetes/client-node": "0.17.0" }, "devDependencies": { "@backstage/cli": "workspace:^" diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 2272804b22..1cf6922e08 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-kubernetes +## 0.7.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-kubernetes-common@0.4.5-next.1 + +## 0.7.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/plugin-kubernetes-common@0.4.5-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## 0.7.4 + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-kubernetes-common@0.4.4 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## 0.7.4-next.1 + +### Patch Changes + +- cfb30b700c: Pin `@kubernetes/client-node` version to `0.17.0`. +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/plugin-kubernetes-common@0.4.4-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.7.4-next.0 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index ee4da4b55f..1ad133c682 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.7.4-next.0", + "version": "0.7.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -40,7 +40,7 @@ "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/theme": "workspace:^", - "@kubernetes/client-node": "^0.17.0", + "@kubernetes/client-node": "0.17.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -66,7 +66,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx index da53db687b..f221a36cee 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import * as deployments from '../../__fixtures__/2-deployments.json'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, textContentMatcher } from '@backstage/test-utils'; import { DeploymentDrawer } from './DeploymentDrawer'; describe('DeploymentDrawer', () => { @@ -33,9 +33,13 @@ describe('DeploymentDrawer', () => { expect(getByText('YAML')).toBeInTheDocument(); expect(getByText('Strategy')).toBeInTheDocument(); expect(getByText('Rolling Update:')).toBeInTheDocument(); - expect(getByText('Max Surge: 25%')).toBeInTheDocument(); - expect(getByText('Max Unavailable: 25%')).toBeInTheDocument(); - expect(getByText('Type: RollingUpdate')).toBeInTheDocument(); + expect(getByText(textContentMatcher('Max Surge: 25%'))).toBeInTheDocument(); + expect( + getByText(textContentMatcher('Max Unavailable: 25%')), + ).toBeInTheDocument(); + expect( + getByText(textContentMatcher('Type: RollingUpdate')), + ).toBeInTheDocument(); expect(getByText('Min Ready Seconds')).toBeInTheDocument(); expect(getByText('???')).toBeInTheDocument(); expect(getByText('Progress Deadline Seconds')).toBeInTheDocument(); diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.test.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.test.tsx index f0146cc22e..f478278b02 100644 --- a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.test.tsx +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import * as ingresses from './__fixtures__/2-ingresses.json'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { textContentMatcher, wrapInTestApp } from '@backstage/test-utils'; import { IngressDrawer } from './IngressDrawer'; describe('IngressDrawer', () => { @@ -28,11 +28,17 @@ describe('IngressDrawer', () => { ), ); - expect(getAllByText('awesome-service')).toHaveLength(2); + expect(getAllByText('awesome-service')).toHaveLength(4); expect(getByText('YAML')).toBeInTheDocument(); expect(getByText('Rules')).toBeInTheDocument(); - expect(getByText('Host: api.awesome-host.io')).toBeInTheDocument(); - expect(getAllByText('Service Port: 80')).toHaveLength(2); - expect(getAllByText('Service Name: awesome-service')).toHaveLength(2); + expect( + getByText(textContentMatcher('Host: api.awesome-host.io')), + ).toBeInTheDocument(); + expect(getAllByText(textContentMatcher('Service Port: 80'))).toHaveLength( + 2, + ); + expect( + getAllByText(textContentMatcher('Service Name: awesome-service')), + ).toHaveLength(2); }); }); diff --git a/plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx b/plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx index c3b39e9f70..b11093250d 100644 --- a/plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx +++ b/plugins/kubernetes/src/components/Pods/PodDrawer.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import * as pod from './__fixtures__/pod.json'; import * as crashingPod from './__fixtures__/crashing-pod.json'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { textContentMatcher, wrapInTestApp } from '@backstage/test-utils'; import { PodDrawer } from './PodDrawer'; describe('PodDrawer', () => { @@ -46,8 +46,10 @@ describe('PodDrawer', () => { expect(getAllByText('True')).toHaveLength(4); expect(getByText('Exposed Ports')).toBeInTheDocument(); expect(getByText('Nginx:')).toBeInTheDocument(); - expect(getByText('Container Port: 80')).toBeInTheDocument(); - expect(getByText('Protocol: TCP')).toBeInTheDocument(); + expect( + getByText(textContentMatcher('Container Port: 80')), + ).toBeInTheDocument(); + expect(getByText(textContentMatcher('Protocol: TCP'))).toBeInTheDocument(); }); it('should render crashing pod', async () => { const { getByText, getAllByText } = render( @@ -80,12 +82,18 @@ describe('PodDrawer', () => { getAllByText('containers with unready status: [side-car other-side-car]'), ).toHaveLength(2); expect(getByText('Exposed Ports')).toBeInTheDocument(); - expect(getAllByText('Protocol: TCP')).toHaveLength(3); + expect(getAllByText(textContentMatcher('Protocol: TCP'))).toHaveLength(3); expect(getByText('Nginx:')).toBeInTheDocument(); - expect(getByText('Container Port: 80')).toBeInTheDocument(); + expect( + getByText(textContentMatcher('Container Port: 80')), + ).toBeInTheDocument(); expect(getByText('Side Car:')).toBeInTheDocument(); - expect(getByText('Container Port: 81')).toBeInTheDocument(); + expect( + getByText(textContentMatcher('Container Port: 81')), + ).toBeInTheDocument(); expect(getByText('Other Side Car:')).toBeInTheDocument(); - expect(getByText('Container Port: 82')).toBeInTheDocument(); + expect( + getByText(textContentMatcher('Container Port: 82')), + ).toBeInTheDocument(); }); }); diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.test.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.test.tsx index 79df15d215..7eb51f073b 100644 --- a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.test.tsx +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import * as services from './__fixtures__/2-services.json'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { textContentMatcher, wrapInTestApp } from '@backstage/test-utils'; import { ServiceDrawer } from './ServiceDrawer'; describe('ServiceDrawer', () => { @@ -33,7 +33,11 @@ describe('ServiceDrawer', () => { expect(getByText('YAML')).toBeInTheDocument(); expect(getByText('Cluster IP')).toBeInTheDocument(); expect(getByText('Ports')).toBeInTheDocument(); - expect(getByText('Target Port: 1997')).toBeInTheDocument(); - expect(getByText('App: awesome-service')).toBeInTheDocument(); + expect( + getByText(textContentMatcher('Target Port: 1997')), + ).toBeInTheDocument(); + expect( + getByText(textContentMatcher('App: awesome-service')), + ).toBeInTheDocument(); }); }); diff --git a/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx b/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx index a88c26785d..c187c7d05d 100644 --- a/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx +++ b/plugins/kubernetes/src/components/StatefulSetsAccordions/StatefulSetDrawer.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import * as statefulsets from '../../__fixtures__/2-statefulsets.json'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, textContentMatcher } from '@backstage/test-utils'; import { StatefulSetDrawer } from './StatefulSetDrawer'; describe('StatefulSetDrawer', () => { @@ -28,19 +28,25 @@ describe('StatefulSetDrawer', () => { />, ); - expect(getAllByText('dice-roller')).toHaveLength(3); + expect(getAllByText('dice-roller')).toHaveLength(4); expect(getByText('StatefulSet')).toBeInTheDocument(); expect(getByText('YAML')).toBeInTheDocument(); - expect(getByText('Type: RollingUpdate')).toBeInTheDocument(); + expect( + getByText(textContentMatcher('Type: RollingUpdate')), + ).toBeInTheDocument(); expect(getByText('Rolling Update:')).toBeInTheDocument(); - expect(getByText('Max Surge: 25%')).toBeInTheDocument(); - expect(getByText('Max Unavailable: 25%')).toBeInTheDocument(); + expect(getByText(textContentMatcher('Max Surge: 25%'))).toBeInTheDocument(); + expect( + getByText(textContentMatcher('Max Unavailable: 25%')), + ).toBeInTheDocument(); expect(getByText('Pod Management Policy')).toBeInTheDocument(); expect(getByText('Parallel')).toBeInTheDocument(); expect(getByText('Service Name')).toBeInTheDocument(); expect(getByText('Selector')).toBeInTheDocument(); expect(getByText('Match Labels:')).toBeInTheDocument(); - expect(getByText('App: dice-roller')).toBeInTheDocument(); + expect( + getByText(textContentMatcher('App: dice-roller')), + ).toBeInTheDocument(); expect(getByText('Revision History Limit')).toBeInTheDocument(); expect(getByText('10')).toBeInTheDocument(); expect(getByText('namespace: default')).toBeInTheDocument(); diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 8a5a1bee97..ce7bf7e787 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-lighthouse +## 0.3.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.3.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + +## 0.3.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + +## 0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 8c87e8eec3..af732e93c6 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.3.11-next.0", + "version": "0.3.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -60,7 +60,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx index 53de33d21b..3e34eed6e3 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx @@ -122,7 +122,7 @@ describe('', () => { it('renders nothing', async () => { const { queryByTestId } = subject(); - expect(await queryByTestId('AuditListTable')).toBeNull(); + expect(queryByTestId('AuditListTable')).toBeNull(); }); }); @@ -137,7 +137,7 @@ describe('', () => { it('renders nothing', async () => { const { queryByTestId } = subject(); - expect(await queryByTestId('AuditListTable')).toBeNull(); + expect(queryByTestId('AuditListTable')).toBeNull(); }); }); }); diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx index d8a54928b2..c2ee202841 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx @@ -76,7 +76,7 @@ describe('AuditListTable', () => { if (!website) throw new Error('https://anchor.fm must be present in fixture'); expect( - rendered.queryByText(formatTime(website.lastAudit.timeCreated)), + rendered.getByText(formatTime(website.lastAudit.timeCreated)), ).toBeInTheDocument(); }); diff --git a/plugins/lighthouse/src/components/AuditView/index.test.tsx b/plugins/lighthouse/src/components/AuditView/index.test.tsx index 00b2843a50..c68daddc67 100644 --- a/plugins/lighthouse/src/components/AuditView/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.test.tsx @@ -98,7 +98,7 @@ describe('AuditView', () => { websiteResponse.audits.forEach(a => { expect( - rendered.queryByText(formatTime(a.timeCreated)), + rendered.getByText(formatTime(a.timeCreated)), ).toBeInTheDocument(); }); }); @@ -199,7 +199,7 @@ describe('AuditView', () => { await rendered.findByTestId('audit-sidebar'); - expect(rendered.queryByTestId('progress')).toBeInTheDocument(); + expect(rendered.getByTestId('progress')).toBeInTheDocument(); }); }); @@ -219,7 +219,7 @@ describe('AuditView', () => { await rendered.findByTestId('audit-sidebar'); - expect(rendered.queryByText(/This audit failed/)).toBeInTheDocument(); + expect(rendered.getByText(/This audit failed/)).toBeInTheDocument(); }); }); }); diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx index 58a3033a1f..ab1e2683b6 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.test.tsx @@ -151,10 +151,10 @@ describe('', () => { it('renders nothing', async () => { const { queryByTestId } = subject(); - expect(await queryByTestId('AuditListTable')).toBeNull(); + expect(queryByTestId('AuditListTable')).toBeNull(); }); }); - // + describe('where there is no data', () => { beforeEach(() => { (useWebsiteForEntity as jest.Mock).mockReturnValue({ @@ -166,7 +166,7 @@ describe('', () => { it('renders nothing', async () => { const { queryByTestId } = subject(); - expect(await queryByTestId('AuditListTable')).toBeNull(); + expect(queryByTestId('AuditListTable')).toBeNull(); }); }); }); diff --git a/plugins/lighthouse/src/components/Intro/index.test.tsx b/plugins/lighthouse/src/components/Intro/index.test.tsx index ed4a11ff43..173dce1315 100644 --- a/plugins/lighthouse/src/components/Intro/index.test.tsx +++ b/plugins/lighthouse/src/components/Intro/index.test.tsx @@ -25,7 +25,7 @@ describe('LighthouseIntro', () => { it('renders successfully', () => { const rendered = render(wrapInTestApp()); expect( - rendered.queryByText('Welcome to Lighthouse in Backstage!'), + rendered.getByText('Welcome to Lighthouse in Backstage!'), ).toBeInTheDocument(); }); @@ -35,7 +35,7 @@ describe('LighthouseIntro', () => { it('selects the first text element', () => { const rendered = render(wrapInTestApp()); - expect(rendered.queryByText(firstTabRe)).toBeInTheDocument(); + expect(rendered.getByText(firstTabRe)).toBeInTheDocument(); expect(rendered.queryByText(secondTabRe)).not.toBeInTheDocument(); }); @@ -43,7 +43,7 @@ describe('LighthouseIntro', () => { const rendered = render(wrapInTestApp()); fireEvent.click(rendered.getByText('Setup')); expect(rendered.queryByText(firstTabRe)).not.toBeInTheDocument(); - expect(rendered.queryByText(secondTabRe)).toBeInTheDocument(); + expect(rendered.getByText(secondTabRe)).toBeInTheDocument(); }); }); diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md index 81efe251d3..63b64379f5 100644 --- a/plugins/newrelic-dashboard/CHANGELOG.md +++ b/plugins/newrelic-dashboard/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-newrelic-dashboard +## 0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.2.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + +## 0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.2.4-next.0 ### Patch Changes diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index a8100bdd38..0c8fa7eaa3 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic-dashboard", - "version": "0.2.4-next.0", + "version": "0.2.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index 526010f2f8..7b2da9ac23 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-newrelic +## 0.3.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## 0.3.30-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## 0.3.29 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## 0.3.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + ## 0.3.29-next.0 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 0eedad1ced..91ea1f286e 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-newrelic", "description": "A Backstage plugin that integrates towards New Relic", - "version": "0.3.29-next.0", + "version": "0.3.30-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -54,7 +54,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/org-react/.eslintrc.js b/plugins/org-react/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/org-react/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md new file mode 100644 index 0000000000..92519e975c --- /dev/null +++ b/plugins/org-react/CHANGELOG.md @@ -0,0 +1,59 @@ +# @backstage/plugin-org-react + +## 0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.1-next.0 + +### Patch Changes + +- 4cb5066828: Bug fixes and adding the possibility to add a default value for the `GroupListPicker`. Fixes: Vertical size jump on text entry, left align for text, selecting a value closes the popup, auto focus on the popup when opening +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.0 + +### Minor Changes + +- e96274f1fe: Implemented the org-react plugin, with it's first component being: a `GroupListPicker` component that will give the user the ability to choose a group + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/theme@0.2.16 + +## 0.1.0-next.0 + +### Minor Changes + +- e96274f1fe: Implemented the org-react plugin, with it's first component being: a `GroupListPicker` component that will give the user the ability to choose a group + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 diff --git a/plugins/org-react/README.md b/plugins/org-react/README.md new file mode 100644 index 0000000000..1f87a5e7e6 --- /dev/null +++ b/plugins/org-react/README.md @@ -0,0 +1,31 @@ +# org-react + +## features + +- Group list picker component + +### GroupListPicker + +The `GroupListPicker` component displays a select box which also has autocomplete functionality. + +To use the `GroupListPicker` component you'll need to import it and add it to your desired place. + +```diff ++ import { GroupListPicker } from '@backstage/plugin-org-react'; ++ import React, { useState } from 'react'; + ++ const [group, setGroup] = useState(); + + + ++ + + +``` + +The `GroupListPicker` comes with four props: + +- `groupTypes`: gives the user the option which group types the component should load. If no value is provided all group types will be loaded in; +- `placeholder`: the placeholder that the select box in the component should display. This might be helpful in informing your users what the functionality of the component is. +- `onChange`: a prop to help the user to give access to the selected group +- `defaultValue`: gives the user the option to define a default value that will be shown initially before making a selection diff --git a/plugins/org-react/api-report.md b/plugins/org-react/api-report.md new file mode 100644 index 0000000000..48bd090f64 --- /dev/null +++ b/plugins/org-react/api-report.md @@ -0,0 +1,22 @@ +## API Report File for "@backstage/plugin-org-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { GroupEntity } from '@backstage/catalog-model'; + +// @public (undocumented) +export const GroupListPicker: (props: GroupListPickerProps) => JSX.Element; + +// @public +export type GroupListPickerProps = { + defaultValue?: string; + placeholder?: string; + groupTypes?: Array; + onChange: (value: GroupEntity | undefined) => void; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json new file mode 100644 index 0000000000..63fe5f2592 --- /dev/null +++ b/plugins/org-react/package.json @@ -0,0 +1,62 @@ +{ + "name": "@backstage/plugin-org-react", + "version": "0.1.1-next.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/org-react" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.57", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.49.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/org-react/src/components/GroupListPicker/GroupListPicker.test.tsx b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.test.tsx new file mode 100644 index 0000000000..21b85ffe5c --- /dev/null +++ b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { ApiProvider } from '@backstage/core-app-api'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { CatalogApi } from '@backstage/catalog-client'; +import { GroupListPicker } from '../GroupListPicker'; +import { GroupEntity } from '@backstage/catalog-model'; +import { TestApiRegistry } from '@backstage/test-utils'; + +const mockGroups: GroupEntity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + namespace: 'default', + name: 'group-a', + }, + spec: { + type: 'org', + profile: { + displayName: 'Group A', + }, + children: [], + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + namespace: 'default', + name: 'group-b', + }, + spec: { + type: 'department', + profile: { + displayName: 'Group B', + }, + children: [], + }, + }, +]; + +const mockCatalogApi = { + getEntities: () => Promise.resolve({ items: mockGroups }), +} as Partial; + +const apis = TestApiRegistry.from([catalogApiRef, mockCatalogApi]); + +describe('', () => { + it('can choose a group', async () => { + const { getByText, getByTestId } = render( + + {}} + /> + , + ); + + await userEvent.click(getByTestId('group-list-picker-button')); + const input = getByTestId('group-list-picker-input').querySelector('input'); + await userEvent.type(input as HTMLElement, 'GR'); + + await waitFor(async () => { + expect(getByText('Group A')).toBeInTheDocument(); + await userEvent.click(getByText('Group A')); + expect(getByText('Group A')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx new file mode 100644 index 0000000000..a47c346390 --- /dev/null +++ b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx @@ -0,0 +1,124 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback } from 'react'; +import { + catalogApiRef, + humanizeEntityRef, +} from '@backstage/plugin-catalog-react'; +import TextField from '@material-ui/core/TextField'; +import Autocomplete from '@material-ui/lab/Autocomplete'; +import useAsync from 'react-use/lib/useAsync'; +import Popover from '@material-ui/core/Popover'; +import { useApi } from '@backstage/core-plugin-api'; +import { ResponseErrorPanel } from '@backstage/core-components'; +import { Entity, GroupEntity } from '@backstage/catalog-model'; +import { GroupListPickerButton } from './GroupListPickerButton'; + +/** + * Props for {@link GroupListPicker}. + * + * @public + */ +export type GroupListPickerProps = { + defaultValue?: string; + placeholder?: string; + groupTypes?: Array; + onChange: (value: GroupEntity | undefined) => void; +}; + +/** @public */ +export const GroupListPicker = (props: GroupListPickerProps) => { + const catalogApi = useApi(catalogApiRef); + + const { onChange, groupTypes, placeholder = '', defaultValue = '' } = props; + const [anchorEl, setAnchorEl] = React.useState(null); + const [inputValue, setInputValue] = React.useState(defaultValue); + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + + const { + loading, + error, + value: groups, + } = useAsync(async () => { + const groupsList = await catalogApi.getEntities({ + filter: { + kind: 'Group', + 'spec.type': groupTypes || [], + }, + }); + + return groupsList.items as GroupEntity[]; + }, [catalogApi, groupTypes]); + + const handleChange = useCallback( + (_, v: GroupEntity | null) => { + onChange(v ?? undefined); + setAnchorEl(null); + }, + [onChange], + ); + + if (error) { + return ; + } + + const getHumanEntityRef = (entity: Entity) => humanizeEntityRef(entity); + + return ( + <> + + option.spec.type} + getOptionLabel={option => + option.spec.profile?.displayName ?? getHumanEntityRef(option) + } + inputValue={inputValue} + onInputChange={(_, value) => setInputValue(value)} + onChange={handleChange} + style={{ width: '300px' }} + renderInput={params => ( + + )} + /> + + + + ); +}; diff --git a/plugins/org-react/src/components/GroupListPicker/GroupListPickerButton.tsx b/plugins/org-react/src/components/GroupListPicker/GroupListPickerButton.tsx new file mode 100644 index 0000000000..01ffe7e58b --- /dev/null +++ b/plugins/org-react/src/components/GroupListPicker/GroupListPickerButton.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { BackstageTheme } from '@backstage/theme'; +import { makeStyles, Typography, Button } from '@material-ui/core'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import PeopleIcon from '@material-ui/icons/People'; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + btn: { + padding: '10px', + width: '100%', + cursor: 'pointer', + justifyContent: 'space-between', + }, + title: { + fontSize: '1.5rem', + fontStyle: 'normal', + fontWeight: theme.typography.fontWeightBold, + height: '32px', + letterSpacing: '-0.25px', + marginBottom: 0, + marginLeft: '4px', + textAlign: 'left', + textTransform: 'none', + width: '100%', + }, + icon: { + transform: 'scale(1.5)', + }, +})); + +type GroupListPickerButtonProps = { + handleClick: (event: React.MouseEvent) => void; + group: string | undefined; +}; + +/** @public */ +export const GroupListPickerButton = (props: GroupListPickerButtonProps) => { + const { handleClick, group } = props; + const classes = useStyles(); + + return ( + + ); +}; diff --git a/plugins/org-react/src/components/GroupListPicker/index.ts b/plugins/org-react/src/components/GroupListPicker/index.ts new file mode 100644 index 0000000000..fe1ba8e1ee --- /dev/null +++ b/plugins/org-react/src/components/GroupListPicker/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { GroupListPicker } from './GroupListPicker'; +export type { GroupListPickerProps } from './GroupListPicker'; diff --git a/plugins/org-react/src/index.ts b/plugins/org-react/src/index.ts new file mode 100644 index 0000000000..c3393bee48 --- /dev/null +++ b/plugins/org-react/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { GroupListPicker } from './components/GroupListPicker'; +export type { GroupListPickerProps } from './components/GroupListPicker'; diff --git a/plugins/org-react/src/setupTests.ts b/plugins/org-react/src/setupTests.ts new file mode 100644 index 0000000000..9bb3e72355 --- /dev/null +++ b/plugins/org-react/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 682da2da91..81a2f60549 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-org +## 0.6.2-next.1 + +### Patch Changes + +- 67cedfe42e: Update `UserProfileCard` and `GroupProfileCard` to not render links unless the `showLinks` prop is set. The primary component for rendering links are the `EntityLinksCard` from plugin-catalog. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.6.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.6.0 + +### Minor Changes + +- 0b11500151: Updates the User and Group Profile cards to add the links from the UserEntity or the GroupEntity + +### Patch Changes + +- 38dd29ea95: Add entity kind to the Ownership Cards. Fix the query parameters for the links of the Ownership Cards so that the catalog page actually selects the right entity kind. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.6.0-next.1 + +### Minor Changes + +- 0b11500151: Updates the User and Group Profile cards to add the links from the UserEntity or the GroupEntity + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.5.11-next.0 ### Patch Changes diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md index f8cf8afe22..e950b1bc8b 100644 --- a/plugins/org/api-report.md +++ b/plugins/org/api-report.md @@ -13,6 +13,7 @@ import { InfoCardVariants } from '@backstage/core-components'; // @public (undocumented) export const EntityGroupProfileCard: (props: { variant?: InfoCardVariants | undefined; + showLinks?: boolean | undefined; }) => JSX.Element; // @public (undocumented) @@ -32,11 +33,13 @@ export const EntityOwnershipCard: (props: { // @public (undocumented) export const EntityUserProfileCard: (props: { variant?: InfoCardVariants | undefined; + showLinks?: boolean | undefined; }) => JSX.Element; // @public (undocumented) export const GroupProfileCard: (props: { variant?: InfoCardVariants; + showLinks?: boolean; }) => JSX.Element; // @public (undocumented) @@ -75,5 +78,6 @@ export const OwnershipCard: (props: { // @public (undocumented) export const UserProfileCard: (props: { variant?: InfoCardVariants; + showLinks?: boolean; }) => JSX.Element; ``` diff --git a/plugins/org/package.json b/plugins/org/package.json index 507660f1a3..e85caf83b3 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.5.11-next.0", + "version": "0.6.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -57,7 +57,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx index fc45d9a8fe..cfb3d847a4 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx @@ -87,3 +87,44 @@ export default { ), ], }; + +const extraDetailsEntity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'team-a', + description: 'Team A', + links: [ + { + url: 'slack://user?team=T00000000&id=U00000000', + title: 'Slack', + icon: 'chat', + }, + { + url: 'https://www.google.com', + title: 'Google', + }, + ], + }, + spec: { + profile: { + displayName: 'Team A', + email: 'team-a@example.com', + picture: + 'https://avatars.dicebear.com/api/identicon/team-a@example.com.svg?background=%23fff&margin=25', + }, + type: 'group', + children: [], + }, + relations: [dummyDepartment], +}; + +export const ExtraDetails = () => ( + + + + + + + +); diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index dfdb566b20..5e795b2710 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -15,44 +15,46 @@ */ import { + ANNOTATION_EDIT_URL, + ANNOTATION_LOCATION, GroupEntity, RELATION_CHILD_OF, RELATION_PARENT_OF, - ANNOTATION_LOCATION, stringifyEntityRef, - ANNOTATION_EDIT_URL, } from '@backstage/catalog-model'; -import { - catalogApiRef, - EntityRefLinks, - getEntityRelations, - useEntity, -} from '@backstage/plugin-catalog-react'; -import { - Box, - Grid, - List, - ListItem, - ListItemIcon, - ListItemText, - Tooltip, - IconButton, -} from '@material-ui/core'; -import AccountTreeIcon from '@material-ui/icons/AccountTree'; -import EmailIcon from '@material-ui/icons/Email'; -import GroupIcon from '@material-ui/icons/Group'; -import EditIcon from '@material-ui/icons/Edit'; -import CachedIcon from '@material-ui/icons/Cached'; -import Alert from '@material-ui/lab/Alert'; -import React, { useCallback } from 'react'; import { Avatar, InfoCard, InfoCardVariants, Link, } from '@backstage/core-components'; +import { + Box, + Grid, + IconButton, + List, + ListItem, + ListItemIcon, + ListItemText, + Tooltip, +} from '@material-ui/core'; +import { + EntityRefLinks, + catalogApiRef, + getEntityRelations, + useEntity, +} from '@backstage/plugin-catalog-react'; +import React, { useCallback } from 'react'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; +import AccountTreeIcon from '@material-ui/icons/AccountTree'; +import Alert from '@material-ui/lab/Alert'; +import CachedIcon from '@material-ui/icons/Cached'; +import EditIcon from '@material-ui/icons/Edit'; +import EmailIcon from '@material-ui/icons/Email'; +import GroupIcon from '@material-ui/icons/Group'; +import { LinksGroup } from '../../Meta'; + const CardTitle = (props: { title: string }) => ( @@ -61,7 +63,10 @@ const CardTitle = (props: { title: string }) => ( ); /** @public */ -export const GroupProfileCard = (props: { variant?: InfoCardVariants }) => { +export const GroupProfileCard = (props: { + variant?: InfoCardVariants; + showLinks?: boolean; +}) => { const catalogApi = useApi(catalogApiRef); const alertApi = useApi(alertApiRef); const { entity: group } = useEntity(); @@ -76,7 +81,7 @@ export const GroupProfileCard = (props: { variant?: InfoCardVariants }) => { } const { - metadata: { name, description, annotations }, + metadata: { name, description, annotations, links }, spec: { profile }, } = group; @@ -190,6 +195,7 @@ export const GroupProfileCard = (props: { variant?: InfoCardVariants }) => { secondary="Child Groups" /> + {props?.showLinks && } diff --git a/plugins/org/src/components/Cards/Meta/LinksGroup.tsx b/plugins/org/src/components/Cards/Meta/LinksGroup.tsx new file mode 100644 index 0000000000..eeade18411 --- /dev/null +++ b/plugins/org/src/components/Cards/Meta/LinksGroup.tsx @@ -0,0 +1,66 @@ +/* + * 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 { EntityLink } from '@backstage/catalog-model'; +import { IconComponent, useApp } from '@backstage/core-plugin-api'; +import LanguageIcon from '@material-ui/icons/Language'; +import { + ListItem, + ListItemIcon, + ListItemText, + Divider, +} from '@material-ui/core'; +import React from 'react'; + +const WebLink = ({ + href, + Icon, + text, +}: { + href: string; + text?: string; + Icon?: IconComponent; +}) => ( + + {Icon ? : } + {text} + +); + +export const LinksGroup = ({ links }: { links?: EntityLink[] }) => { + const app = useApp(); + const iconResolver = (key?: string): IconComponent => + key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon; + + if (links === undefined) { + return null; + } + + return ( + <> + + {links.map(link => { + return ( + + ); + })} + + ); +}; diff --git a/plugins/org/src/components/Cards/Meta/index.ts b/plugins/org/src/components/Cards/Meta/index.ts new file mode 100644 index 0000000000..076af49bf9 --- /dev/null +++ b/plugins/org/src/components/Cards/Meta/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './LinksGroup'; diff --git a/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx b/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx index 6f7680849c..f996342454 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx @@ -56,15 +56,17 @@ const useStyles = makeStyles((theme: BackstageTheme) => const EntityCountTile = ({ counter, type, - name, + kind, url, }: { counter: number; - type: string; - name: string; + type?: string; + kind: string; url: string; }) => { - const classes = useStyles({ type }); + const classes = useStyles({ type: type ?? kind }); + + const rawTitle = type ?? kind; return ( @@ -78,8 +80,9 @@ const EntityCountTile = ({ {counter} - {pluralize(name, counter)} + {pluralize(rawTitle.toLocaleUpperCase('en-US'), counter)} + {type && {kind}} ); @@ -113,11 +116,11 @@ export const ComponentsGrid = ({ return ( {componentsWithCounters?.map(c => ( - + diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index 28fa3f4988..84f27fa183 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -217,10 +217,16 @@ describe('OwnershipCard', () => { expect( queryByText(getByText('SYSTEM').parentElement!, '1'), ).toBeInTheDocument(); + expect( + queryByText(getByText('SYSTEM').parentElement!, 'System'), + ).not.toBeInTheDocument(); expect(getByText('OPENAPI')).toBeInTheDocument(); expect( queryByText(getByText('OPENAPI').parentElement!, '1'), ).toBeInTheDocument(); + expect( + queryByText(getByText('OPENAPI').parentElement!, 'API'), + ).toBeInTheDocument(); expect(() => getByText('LIBRARY')).toThrow(); }); @@ -246,7 +252,7 @@ describe('OwnershipCard', () => { expect(getByText('OPENAPI').closest('a')).toHaveAttribute( 'href', - '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D=my-team&filters%5Buser%5D=all', + '/create/?filters%5Bkind%5D=api&filters%5Btype%5D=openapi&filters%5Bowners%5D=my-team&filters%5Buser%5D=all', ); }); @@ -292,7 +298,7 @@ describe('OwnershipCard', () => { expect(getByText('OPENAPI').closest('a')).toHaveAttribute( 'href', - '/create/?filters%5Bkind%5D=API&filters%5Btype%5D=openapi&filters%5Bowners%5D=user%3Athe-user&filters%5Bowners%5D=my-team&filters%5Bowners%5D=custom%2Fsome-team&filters%5Buser%5D=all', + '/create/?filters%5Bkind%5D=api&filters%5Btype%5D=openapi&filters%5Bowners%5D=user%3Athe-user&filters%5Bowners%5D=my-team&filters%5Bowners%5D=custom%2Fsome-team&filters%5Buser%5D=all', ); }); diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts index 7c0bb76e2e..c928d9b55f 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts @@ -36,7 +36,7 @@ const limiter = limiterFactory(10); type EntityTypeProps = { kind: string; - type: string; + type?: string; count: number; }; @@ -49,7 +49,7 @@ const getQueryParams = ( humanizeEntityRef(parseEntityRef(owner), { defaultKind: 'group' }), ); const filters = { - kind, + kind: kind.toLocaleLowerCase('en-US'), type, owners, user: 'all', @@ -135,7 +135,7 @@ export function useGetEntities( | { counter: number; type: string; - name: string; + kind: string; queryParams: string; }[] | undefined; @@ -173,16 +173,14 @@ export function useGetEntities( const counts = ownedEntitiesList.items.reduce( (acc: EntityTypeProps[], ownedEntity) => { const match = acc.find( - x => - x.kind === ownedEntity.kind && - x.type === (ownedEntity.spec?.type ?? ownedEntity.kind), + x => x.kind === ownedEntity.kind && x.type === ownedEntity.spec?.type, ); if (match) { match.count += 1; } else { acc.push({ kind: ownedEntity.kind, - type: ownedEntity.spec?.type?.toString() ?? ownedEntity.kind, + type: ownedEntity.spec?.type?.toString(), count: 1, }); } @@ -197,12 +195,12 @@ export function useGetEntities( return topN.map(topOwnedEntity => ({ counter: topOwnedEntity.count, type: topOwnedEntity.type, - name: topOwnedEntity.type.toLocaleUpperCase('en-US'), + kind: topOwnedEntity.kind, queryParams: getQueryParams(owners, topOwnedEntity), })) as Array<{ counter: number; type: string; - name: string; + kind: string; queryParams: string; }>; }, [catalogApi, entity, relationsType]); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx index eb7b8fadda..a27be7857b 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx @@ -102,3 +102,43 @@ export default { }), ], }; + +const extraDetailsEntity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'guest', + description: 'Description for guest', + links: [ + { + url: 'slack://user?team=T00000000&id=U00000000', + title: 'Slack', + icon: 'chat', + }, + { + url: 'https://www.google.com', + title: 'Google', + }, + ], + }, + spec: { + profile: { + displayName: 'Guest User', + email: 'guest@example.com', + picture: + 'https://avatars.dicebear.com/api/avataaars/guest@example.com.svg?background=%23fff', + }, + memberOf: ['team-a'], + }, + relations: [dummyGroup], +}; + +export const ExtraDetails = () => ( + + + + + + + +); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx index f339a5828c..547c636d22 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx @@ -155,4 +155,112 @@ describe('Edit Button', () => { ); expect(rendered.getByRole('button')).toBeInTheDocument(); }); + + it('Should not show links by default', async () => { + const annotations: Record = { + 'backstage.io/edit-url': 'https://example.com/user.yaml', + }; + const userEntity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'calum.leavy', + description: 'Super awesome human', + annotations, + links: [ + { + url: 'slack://user?team=T00000000&id=U00000000', + title: 'Slack', + icon: 'message', + }, + { + url: 'https://www.google.com', + title: 'Google', + }, + ], + }, + spec: { + profile: { + displayName: 'Calum Leavy', + email: 'calum-leavy@example.com', + }, + memberOf: ['ExampleGroup'], + }, + relations: [ + { + type: 'memberOf', + targetRef: 'group:default/examplegroup', + }, + ], + }; + + const rendered = await renderWithEffects( + wrapInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ), + ); + expect(rendered.queryByText('Slack')).toBeNull(); + expect(rendered.queryByText('Google')).toBeNull(); + }); + + it('Should show the links if showLinks is set', async () => { + const annotations: Record = { + 'backstage.io/edit-url': 'https://example.com/user.yaml', + }; + const userEntity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'calum.leavy', + description: 'Super awesome human', + annotations, + links: [ + { + url: 'slack://user?team=T00000000&id=U00000000', + title: 'Slack', + icon: 'message', + }, + { + url: 'https://www.google.com', + title: 'Google', + }, + ], + }, + spec: { + profile: { + displayName: 'Calum Leavy', + email: 'calum-leavy@example.com', + }, + memberOf: ['ExampleGroup'], + }, + relations: [ + { + type: 'memberOf', + targetRef: 'group:default/examplegroup', + }, + ], + }; + + const rendered = await renderWithEffects( + wrapInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ), + ); + expect(rendered.getByText('Slack')).toBeInTheDocument(); + expect(rendered.getByText('Google')).toBeInTheDocument(); + }); }); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index dd75578687..a45658b3a6 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -15,15 +15,16 @@ */ import { + ANNOTATION_EDIT_URL, RELATION_MEMBER_OF, UserEntity, - ANNOTATION_EDIT_URL, } from '@backstage/catalog-model'; import { - EntityRefLinks, - getEntityRelations, - useEntity, -} from '@backstage/plugin-catalog-react'; + Avatar, + InfoCard, + InfoCardVariants, + Link, +} from '@backstage/core-components'; import { Box, Grid, @@ -34,18 +35,19 @@ import { ListItemText, Tooltip, } from '@material-ui/core'; +import { + EntityRefLinks, + getEntityRelations, + useEntity, +} from '@backstage/plugin-catalog-react'; + +import Alert from '@material-ui/lab/Alert'; import EditIcon from '@material-ui/icons/Edit'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; +import { LinksGroup } from '../../Meta'; import PersonIcon from '@material-ui/icons/Person'; -import Alert from '@material-ui/lab/Alert'; import React from 'react'; -import { - Avatar, - InfoCard, - InfoCardVariants, - Link, -} from '@backstage/core-components'; const CardTitle = (props: { title?: string }) => props.title ? ( @@ -56,7 +58,10 @@ const CardTitle = (props: { title?: string }) => ) : null; /** @public */ -export const UserProfileCard = (props: { variant?: InfoCardVariants }) => { +export const UserProfileCard = (props: { + variant?: InfoCardVariants; + showLinks?: boolean; +}) => { const { entity: user } = useEntity(); if (!user) { return User not found; @@ -66,7 +71,7 @@ export const UserProfileCard = (props: { variant?: InfoCardVariants }) => { user.metadata.annotations?.[ANNOTATION_EDIT_URL]; const { - metadata: { name: metaName, description }, + metadata: { name: metaName, description, links }, spec: { profile }, } = user; const displayName = profile?.displayName ?? metaName; @@ -128,6 +133,8 @@ export const UserProfileCard = (props: { variant?: InfoCardVariants }) => { /> + + {props?.showLinks && } diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index ec4a152a4d..9e5a182633 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/plugin-pagerduty +## 0.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.5.5-next.0 + +### Patch Changes + +- cb716004ef: Internal refactor to improve tests +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.5.4 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.5.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.5.4-next.0 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 64ab795489..9bd4e80476 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.5.4-next.0", + "version": "0.5.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -61,7 +61,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 7266d5e9eb..7d99be219d 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -73,7 +73,7 @@ export class PagerDutyClient implements PagerDutyApi { url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/pagerduty/services?${commonGetServiceParams}&query=${integrationKey}`; - const { services } = await this.getByUrl(url); + const { services } = await this.findByUrl(url); const service = services[0]; if (!service) throw new NotFoundError(); @@ -84,7 +84,7 @@ export class PagerDutyClient implements PagerDutyApi { 'proxy', )}/pagerduty/services/${serviceId}?${commonGetServiceParams}`; - response = await this.getByUrl(url); + response = await this.findByUrl(url); } else { throw new NotFoundError(); } @@ -100,7 +100,7 @@ export class PagerDutyClient implements PagerDutyApi { 'proxy', )}/pagerduty/incidents?${params}`; - return await this.getByUrl(url); + return await this.findByUrl(url); } async getChangeEventsByServiceId( @@ -111,7 +111,7 @@ export class PagerDutyClient implements PagerDutyApi { 'proxy', )}/pagerduty/services/${serviceId}/change_events?${params}`; - return await this.getByUrl(url); + return await this.findByUrl(url); } async getOnCallByPolicyId( @@ -122,7 +122,7 @@ export class PagerDutyClient implements PagerDutyApi { 'proxy', )}/pagerduty/oncalls?${params}`; - return await this.getByUrl(url); + return await this.findByUrl(url); } triggerAlarm(request: PagerDutyTriggerAlarmRequest): Promise { @@ -158,7 +158,7 @@ export class PagerDutyClient implements PagerDutyApi { return this.request(`${url}/enqueue`, options); } - private async getByUrl(url: string): Promise { + private async findByUrl(url: string): Promise { const options = { method: 'GET', headers: { diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx index a83874a995..bd2016eb16 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; -import { render, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { Incidents } from './Incidents'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; @@ -32,15 +33,15 @@ describe('Incidents', () => { .fn() .mockImplementationOnce(async () => ({ incidents: [] })); - const { getByText, queryByTestId } = render( + render( wrapInTestApp( , ), ); - await waitFor(() => !queryByTestId('progress')); - expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + await waitFor(() => !screen.queryByTestId('progress')); + expect(screen.getByText('Nice! No incidents found!')).toBeInTheDocument(); }); it('Renders all incidents', async () => { @@ -85,25 +86,25 @@ describe('Incidents', () => { }, ] as PagerDutyIncident[], })); - const { getByText, getAllByTitle, queryByTestId } = render( + render( wrapInTestApp( , ), ); - await waitFor(() => !queryByTestId('progress')); - expect(getByText('title1')).toBeInTheDocument(); - expect(getByText('title2')).toBeInTheDocument(); - expect(getByText('person1')).toBeInTheDocument(); - expect(getByText('person2')).toBeInTheDocument(); - expect(getByText('triggered')).toBeInTheDocument(); - expect(getByText('acknowledged')).toBeInTheDocument(); - expect(queryByTestId('chip-triggered')).toBeInTheDocument(); - expect(queryByTestId('chip-acknowledged')).toBeInTheDocument(); + await waitFor(() => !screen.queryByTestId('progress')); + expect(screen.getByText('title1')).toBeInTheDocument(); + expect(screen.getByText('title2')).toBeInTheDocument(); + expect(screen.getByText('person1')).toBeInTheDocument(); + expect(screen.getByText('person2')).toBeInTheDocument(); + expect(screen.getByText('triggered')).toBeInTheDocument(); + expect(screen.getByText('acknowledged')).toBeInTheDocument(); + expect(screen.getByTestId('chip-triggered')).toBeInTheDocument(); + expect(screen.getByTestId('chip-acknowledged')).toBeInTheDocument(); // assert links, mailto and hrefs, date calculation - expect(getAllByTitle('View in PagerDuty').length).toEqual(2); + expect(screen.getAllByTitle('View in PagerDuty').length).toEqual(2); }); it('Handle errors', async () => { @@ -111,16 +112,18 @@ describe('Incidents', () => { .fn() .mockRejectedValueOnce(new Error('Error occurred')); - const { getByText, queryByTestId } = render( + render( wrapInTestApp( , ), ); - await waitFor(() => !queryByTestId('progress')); + await waitFor(() => !screen.queryByTestId('progress')); expect( - getByText('Error encountered while fetching information. Error occurred'), + screen.getByText( + 'Error encountered while fetching information. Error occurred', + ), ).toBeInTheDocument(); }); }); diff --git a/plugins/periskop-backend/CHANGELOG.md b/plugins/periskop-backend/CHANGELOG.md index 63ca36cc60..297a8df4a6 100644 --- a/plugins/periskop-backend/CHANGELOG.md +++ b/plugins/periskop-backend/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-periskop-backend +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + +## 0.1.10-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + +## 0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + ## 0.1.9-next.0 ### Patch Changes diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index 3a91af5252..32da64f6b8 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop-backend", - "version": "0.1.9-next.0", + "version": "0.1.10-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/periskop/CHANGELOG.md b/plugins/periskop/CHANGELOG.md index afec1859f2..fbfd9b6090 100644 --- a/plugins/periskop/CHANGELOG.md +++ b/plugins/periskop/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-periskop +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.10-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.9 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.9-next.0 ### Patch Changes diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index 53971e9930..1a840b0519 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop", - "version": "0.1.9-next.0", + "version": "0.1.10-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -50,7 +50,7 @@ "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index d57c79ca08..4e65e9b6b7 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-permission-backend +## 0.5.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/plugin-permission-node@0.7.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## 0.5.14-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-permission-node@0.7.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.5.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/plugin-permission-node@0.7.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.5.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/plugin-permission-node@0.7.1-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + ## 0.5.13-next.0 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 16351d26b9..053a12bb9a 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.13-next.0", + "version": "0.5.14-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,7 +42,7 @@ "@backstage/cli": "workspace:^", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index 31c212bffd..eb9aec2da0 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-permission-common +## 0.7.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.7.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.7.1 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- 64848c963c: Properly handle rules that have no parameters in `PermissionClient` +- Updated dependencies + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + ## 0.7.1-next.0 ### Patch Changes diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 394cbe896f..32ba10e1f0 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-common", "description": "Isomorphic types and client for Backstage permissions and authorization", - "version": "0.7.1-next.0", + "version": "0.7.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -50,6 +50,6 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "msw": "^0.47.0" + "msw": "^0.49.0" } } diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 2d31993117..afb5dabe48 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-permission-node +## 0.7.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## 0.7.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.7.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.7.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + ## 0.7.1-next.0 ### Patch Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 33baf825ac..1bba7787f4 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.7.1-next.0", + "version": "0.7.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -48,7 +48,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index c8e1cb072e..fc6e7301d2 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-permission-react +## 0.4.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## 0.4.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/config@1.0.4 + ## 0.4.7-next.0 ### Patch Changes diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 857e1a3881..4a316b8dcd 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.7-next.0", + "version": "0.4.8-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/playlist-backend/CHANGELOG.md b/plugins/playlist-backend/CHANGELOG.md index afb6a2867f..e9b6298927 100644 --- a/plugins/playlist-backend/CHANGELOG.md +++ b/plugins/playlist-backend/CHANGELOG.md @@ -1,5 +1,84 @@ # @backstage/plugin-playlist-backend +## 0.2.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-test-utils@0.1.31-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/plugin-permission-node@0.7.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-playlist-common@0.1.3-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-test-utils@0.1.31-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-permission-node@0.7.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-playlist-common@0.1.3-next.0 + +## 0.2.1 + +### Patch Changes + +- 605f269f0d: Updated Playlist plugin docs: + + - Updated `playlist` plugin README to include note about installing backend plugin and added images for the various features + - Updated `playlist-backend` plugin README to remove `IdentityClient` import in example as it is not used and made minor change to headings + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/backend-test-utils@0.1.30 + - @backstage/plugin-permission-node@0.7.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-playlist-common@0.1.2 + +## 0.2.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-test-utils@0.1.30-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/plugin-permission-node@0.7.1-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-playlist-common@0.1.2-next.0 + +## 0.2.1-next.1 + +### Patch Changes + +- 605f269f0d: Updated Playlist plugin docs: + + - Updated `playlist` plugin README to include note about installing backend plugin and added images for the various features + - Updated `playlist-backend` plugin README to remove `IdentityClient` import in example as it is not used and made minor change to headings + ## 0.2.1-next.0 ### Patch Changes diff --git a/plugins/playlist-backend/README.md b/plugins/playlist-backend/README.md index 14a39ca741..c0055ebdf7 100644 --- a/plugins/playlist-backend/README.md +++ b/plugins/playlist-backend/README.md @@ -2,21 +2,20 @@ Welcome to the playlist backend plugin! -# Installation +## Installation -## Install the package +### Install the package ```bash # From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-playlist-backend ``` -## Adding the plugin to your `packages/backend` +### Adding the plugin to your `packages/backend` You'll need to add the plugin to the router in your `backend` package. You can do this by creating a file called `packages/backend/src/plugins/playlist.ts` ```tsx -import { IdentityClient } from '@backstage/plugin-auth-node'; import { createRouter } from '@backstage/plugin-playlist-backend'; import { Router } from 'express'; import { PluginEnvironment } from '../types'; diff --git a/plugins/playlist-backend/migrations/20220701011329_init.js b/plugins/playlist-backend/migrations/20220701011329_init.js index be08363dec..fea6b52700 100644 --- a/plugins/playlist-backend/migrations/20220701011329_init.js +++ b/plugins/playlist-backend/migrations/20220701011329_init.js @@ -14,6 +14,11 @@ * limitations under the License. */ +// @ts-check + +/** + * @param { import("knex").Knex } knex + */ exports.up = async function up(knex) { await knex.schema.createTable('playlists', table => { table.comment('Playlists table'); @@ -57,6 +62,9 @@ exports.up = async function up(knex) { }); }; +/** + * @param { import("knex").Knex } knex + */ exports.down = async function down(knex) { await knex.schema.dropTable('playlists'); await knex.schema.dropTable('entities'); diff --git a/plugins/playlist-backend/package.json b/plugins/playlist-backend/package.json index 6bb557d8a3..ab93d75e48 100644 --- a/plugins/playlist-backend/package.json +++ b/plugins/playlist-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist-backend", - "version": "0.2.1-next.0", + "version": "0.2.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -45,7 +45,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/playlist-common/CHANGELOG.md b/plugins/playlist-common/CHANGELOG.md index 7dcf06740e..bfcce06ec5 100644 --- a/plugins/playlist-common/CHANGELOG.md +++ b/plugins/playlist-common/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-playlist-common +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.1 + +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.1 + ## 0.1.2-next.0 ### Patch Changes diff --git a/plugins/playlist-common/package.json b/plugins/playlist-common/package.json index 287c69495a..21ad490c14 100644 --- a/plugins/playlist-common/package.json +++ b/plugins/playlist-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-playlist-common", "description": "Common functionalities for the playlist plugin", - "version": "0.1.2-next.0", + "version": "0.1.3-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/playlist/CHANGELOG.md b/plugins/playlist/CHANGELOG.md index b064b2e5b6..277e92de73 100644 --- a/plugins/playlist/CHANGELOG.md +++ b/plugins/playlist/CHANGELOG.md @@ -1,5 +1,88 @@ # @backstage/plugin-playlist +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-playlist-common@0.1.3-next.1 + +## 0.1.3-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-permission-react@0.4.8-next.0 + - @backstage/plugin-playlist-common@0.1.3-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## 0.1.2 + +### Patch Changes + +- 605f269f0d: Updated Playlist plugin docs: + + - Updated `playlist` plugin README to include note about installing backend plugin and added images for the various features + - Updated `playlist-backend` plugin README to remove `IdentityClient` import in example as it is not used and made minor change to headings + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/plugin-search-react@1.2.1 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-permission-react@0.4.7 + - @backstage/plugin-playlist-common@0.1.2 + +## 0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-permission-react@0.4.7-next.0 + - @backstage/plugin-playlist-common@0.1.2-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + +## 0.1.2-next.1 + +### Patch Changes + +- 605f269f0d: Updated Playlist plugin docs: + + - Updated `playlist` plugin README to include note about installing backend plugin and added images for the various features + - Updated `playlist-backend` plugin README to remove `IdentityClient` import in example as it is not used and made minor change to headings + ## 0.1.2-next.0 ### Patch Changes diff --git a/plugins/playlist/README.md b/plugins/playlist/README.md index 7c814d302b..6ae3fb84ac 100644 --- a/plugins/playlist/README.md +++ b/plugins/playlist/README.md @@ -6,6 +6,14 @@ This plugin allows you to create, share, and follow custom collections of entiti ## Setup +The following sections will help you get the Playlist plugin setup and running + +### Backend + +You need to setup the [Playlist backend plugin](https://github.com/backstage/backstage/tree/master/plugins/playlist-backend) before you move forward with any of these steps if you haven't already + +### Installation + Install this plugin: ```bash @@ -123,6 +131,28 @@ const defaultEntityPage = ( Note: the above only shows an example for the `defaultEntityPage` for a full example of this you can look at [this EntityPage](../../packages/app/src/components/catalog/EntityPage.tsx) +## Features + +### View All Playlists + +![View all playlists example](./docs/playlist-view-all.png) + +### View Playlist + +![View Playlist example](./docs/playlist-view-single.png) + +### Create New Playlist + +![Create New Playlist example](./docs/playlist-create-new.png) + +### Add Entities to Playlist + +![Add Entities to Playlist example](./docs/playlist-add-entities.png) + +### Add to Playlist from Entity + +![Add to Playlist from Entity example](./docs/playlist-add-from-entity.png) + ## Links - [playlist-backend](../playlist-backend) provides the backend API for this frontend. diff --git a/plugins/playlist/docs/playlist-add-entities.png b/plugins/playlist/docs/playlist-add-entities.png new file mode 100644 index 0000000000..a53de49880 Binary files /dev/null and b/plugins/playlist/docs/playlist-add-entities.png differ diff --git a/plugins/playlist/docs/playlist-add-from-entity.png b/plugins/playlist/docs/playlist-add-from-entity.png new file mode 100644 index 0000000000..d41135064a Binary files /dev/null and b/plugins/playlist/docs/playlist-add-from-entity.png differ diff --git a/plugins/playlist/docs/playlist-create-new.png b/plugins/playlist/docs/playlist-create-new.png new file mode 100644 index 0000000000..cc369e26fa Binary files /dev/null and b/plugins/playlist/docs/playlist-create-new.png differ diff --git a/plugins/playlist/docs/playlist-view-all.png b/plugins/playlist/docs/playlist-view-all.png new file mode 100644 index 0000000000..1b65eb1af2 Binary files /dev/null and b/plugins/playlist/docs/playlist-view-all.png differ diff --git a/plugins/playlist/docs/playlist-view-single.png b/plugins/playlist/docs/playlist-view-single.png new file mode 100644 index 0000000000..0890c5013b Binary files /dev/null and b/plugins/playlist/docs/playlist-view-single.png differ diff --git a/plugins/playlist/package.json b/plugins/playlist/package.json index df620fc97a..8f71461129 100644 --- a/plugins/playlist/package.json +++ b/plugins/playlist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist", - "version": "0.1.2-next.0", + "version": "0.1.3-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -12,6 +12,11 @@ "backstage": { "role": "frontend-plugin" }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/playlist" + }, "scripts": { "start": "backstage-cli package start", "build": "backstage-cli package build", @@ -57,7 +62,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.47.0", + "msw": "^0.49.0", "swr": "^1.1.2" }, "files": [ diff --git a/plugins/playlist/src/components/CreatePlaylistButton/CreatePlaylistButton.test.tsx b/plugins/playlist/src/components/CreatePlaylistButton/CreatePlaylistButton.test.tsx index e00f7dc582..ef8ca57b69 100644 --- a/plugins/playlist/src/components/CreatePlaylistButton/CreatePlaylistButton.test.tsx +++ b/plugins/playlist/src/components/CreatePlaylistButton/CreatePlaylistButton.test.tsx @@ -26,7 +26,6 @@ import { fireEvent, waitFor } from '@testing-library/react'; import { act } from '@testing-library/react-hooks'; import React from 'react'; import { SWRConfig } from 'swr'; - import { PlaylistApi, playlistApiRef } from '../../api'; import { rootRouteRef } from '../../routes'; import { CreatePlaylistButton } from './CreatePlaylistButton'; @@ -103,7 +102,7 @@ describe('', () => { fireEvent.click(rendered.getByRole('button')); }); expect( - rendered.queryByTestId('mock-playlist-edit-dialog'), + rendered.getByTestId('mock-playlist-edit-dialog'), ).toBeInTheDocument(); }); diff --git a/plugins/playlist/src/components/EntityPlaylistDialog/EntityPlaylistDialog.test.tsx b/plugins/playlist/src/components/EntityPlaylistDialog/EntityPlaylistDialog.test.tsx index 9c61105ef9..1d9b298e19 100644 --- a/plugins/playlist/src/components/EntityPlaylistDialog/EntityPlaylistDialog.test.tsx +++ b/plugins/playlist/src/components/EntityPlaylistDialog/EntityPlaylistDialog.test.tsx @@ -28,7 +28,6 @@ import { fireEvent, getByRole, waitFor } from '@testing-library/react'; import { act } from '@testing-library/react-hooks'; import React from 'react'; import { SWRConfig } from 'swr'; - import { PlaylistApi, playlistApiRef } from '../../api'; import { rootRouteRef } from '../../routes'; import { EntityPlaylistDialog } from './EntityPlaylistDialog'; @@ -182,7 +181,7 @@ describe('EntityPlaylistDialog', () => { }); expect( - rendered.queryByTestId('mock-playlist-edit-dialog'), + rendered.getByTestId('mock-playlist-edit-dialog'), ).toBeInTheDocument(); act(() => { diff --git a/plugins/playlist/src/components/PersonalListPicker/PersonalListPicker.test.tsx b/plugins/playlist/src/components/PersonalListPicker/PersonalListPicker.test.tsx index 9255ee1bdb..b8ef0d9de0 100644 --- a/plugins/playlist/src/components/PersonalListPicker/PersonalListPicker.test.tsx +++ b/plugins/playlist/src/components/PersonalListPicker/PersonalListPicker.test.tsx @@ -24,7 +24,7 @@ import { } from '@backstage/core-plugin-api'; import { Playlist } from '@backstage/plugin-playlist-common'; import { MockStorageApi, TestApiRegistry } from '@backstage/test-utils'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { MockPlaylistListProvider } from '../../testUtils'; @@ -94,7 +94,7 @@ describe('', () => { }); it('renders filter groups', async () => { - const { queryByText } = render( + render( @@ -103,8 +103,8 @@ describe('', () => { ); await waitFor(() => { - expect(queryByText('Personal')).toBeInTheDocument(); - expect(queryByText('Test Company')).toBeInTheDocument(); + expect(screen.getByText('Personal')).toBeInTheDocument(); + expect(screen.getByText('Test Company')).toBeInTheDocument(); }); }); diff --git a/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.test.tsx b/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.test.tsx index f7720b25c1..21658ef36e 100644 --- a/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.test.tsx +++ b/plugins/playlist/src/components/PlaylistPage/PlaylistEntitiesTable.test.tsx @@ -27,7 +27,6 @@ import { fireEvent, waitFor } from '@testing-library/react'; import { act } from '@testing-library/react-hooks'; import React from 'react'; import { SWRConfig } from 'swr'; - import { PlaylistApi, playlistApiRef } from '../../api'; import { PlaylistEntitiesTable } from './PlaylistEntitiesTable'; @@ -158,7 +157,7 @@ describe('PlaylistEntitiesTable', () => { }); expect( - rendered.queryByTestId('mock-add-entities-drawer'), + rendered.getByTestId('mock-add-entities-drawer'), ).toBeInTheDocument(); act(() => { diff --git a/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.test.tsx b/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.test.tsx index 39b051140e..4594a43a88 100644 --- a/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.test.tsx +++ b/plugins/playlist/src/components/PlaylistPage/PlaylistHeader.test.tsx @@ -31,7 +31,6 @@ import { fireEvent, waitFor } from '@testing-library/react'; import { act } from '@testing-library/react-hooks'; import React from 'react'; import { SWRConfig } from 'swr'; - import { PlaylistApi, playlistApiRef } from '../../api'; import { rootRouteRef } from '../../routes'; import { PlaylistHeader } from './PlaylistHeader'; @@ -122,7 +121,7 @@ describe('PlaylistHeader', () => { testPlaylist.public = false; rendered.rerender(element); - expect(rendered.queryByText('private')).toBeInTheDocument(); + expect(rendered.getByText('private')).toBeInTheDocument(); }); it('has edit and delete options enabled if authorized', async () => { @@ -202,7 +201,7 @@ describe('PlaylistHeader', () => { }); expect( - rendered.queryByTestId('mock-playlist-edit-dialog'), + rendered.getByTestId('mock-playlist-edit-dialog'), ).toBeInTheDocument(); act(() => { diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 91c3419ccd..e6c245dea8 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-proxy-backend +## 0.2.33-next.1 + +### Patch Changes + +- 03843259b4: Documented the `createRouter` method. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + +## 0.2.33-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.2.32 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + +## 0.2.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + ## 0.2.32-next.0 ### Patch Changes diff --git a/plugins/proxy-backend/api-report.md b/plugins/proxy-backend/api-report.md index b6497adbaf..581e7a904c 100644 --- a/plugins/proxy-backend/api-report.md +++ b/plugins/proxy-backend/api-report.md @@ -8,7 +8,7 @@ import express from 'express'; import { Logger } from 'winston'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; -// @public (undocumented) +// @public export function createRouter(options: RouterOptions): Promise; // @public (undocumented) diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 2deee67f17..ef09de88b4 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-proxy-backend", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", - "version": "0.2.32-next.0", + "version": "0.2.33-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -51,7 +51,7 @@ "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", "@types/yup": "^0.29.13", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index b7ee7014bc..224c2c6202 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -178,7 +178,24 @@ export function buildMiddleware( return createProxyMiddleware(filter, fullConfig); } -/** @public */ +/** + * Creates a new {@link https://expressjs.com/en/api.html#router | "express router"} that proxy each target configured under the `proxy` key of the config + * @example + * ```ts + * let router = await createRouter({logger, config, discovery}); + * ``` + * @config + * ```yaml + * proxy: + * simple-example: http://simple.example.com:8080 # Opt 1 Simple URL String + * '/larger-example/v1': # Opt 2 `http-proxy-middleware` compatible object + * target: http://larger.example.com:8080/svc.v1 + * headers: + * Authorization: Bearer ${EXAMPLE_AUTH_TOKEN} + *``` + * @see https://backstage.io/docs/plugins/proxying + * @public + */ export async function createRouter( options: RouterOptions, ): Promise { diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index 962b167902..61c7a3a494 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,38 @@ # @backstage/plugin-rollbar-backend +## 0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + +## 0.1.36-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.1.35 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + +## 0.1.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + ## 0.1.35-next.0 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 0fb2c0a4e0..d52c19b0ca 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar-backend", "description": "A Backstage backend plugin that integrates towards Rollbar", - "version": "0.1.35-next.0", + "version": "0.1.36-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -52,7 +52,7 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 340e06d265..d1d42ab2d2 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-rollbar +## 0.4.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.4.12-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.4.11 + +### Patch Changes + +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.4.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.4.11-next.0 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 1c4b585f5f..0d2e01de48 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.4.11-next.0", + "version": "0.4.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -62,7 +62,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 1a73a7fc41..38402b36c6 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,61 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-scaffolder-backend@1.8.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.2.14-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.1-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-scaffolder-backend@1.8.0 + - @backstage/integration@1.4.0 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.2.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-scaffolder-backend@1.8.0-next.2 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + +## 0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index a42bfdcf60..90aa36a12b 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.2.13-next.0", + "version": "0.2.14-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -40,7 +40,7 @@ "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "mock-fs": "^5.1.0", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 388ad285f1..46e1d6573a 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-scaffolder-backend@1.8.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.1-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.4.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-scaffolder-backend@1.8.0 + - @backstage/integration@1.4.0 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.4.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-scaffolder-backend@1.8.0-next.2 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + +## 0.4.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.1 + ## 0.4.6-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 6a08271e80..35382b898b 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.4.6-next.0", + "version": "0.4.7-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 52654ecc2a..5d39d4e307 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-scaffolder-backend@1.8.1-next.1 + - @backstage/config@1.0.5-next.1 + +## 0.2.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + +## 0.2.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + +## 0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.2 + - @backstage/config@1.0.4-next.0 + - @backstage/types@1.0.1-next.0 + +## 0.2.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.8.0-next.1 + ## 0.2.11-next.0 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index d617fec38e..2f9c74402e 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.2.11-next.0", + "version": "0.2.12-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index caf413992b..8a759ef651 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,141 @@ # @backstage/plugin-scaffolder-backend +## 1.8.1-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-catalog-backend@1.6.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/backend-plugin-api@0.1.5-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/plugin-catalog-node@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-scaffolder-common@1.2.3-next.1 + +## 1.8.1-next.0 + +### Patch Changes + +- cb716004ef: Internal refactor to improve tests +- 26404430bc: Use Json types from @backstage/types +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/plugin-catalog-backend@1.6.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-plugin-api@0.1.5-next.0 + - @backstage/plugin-catalog-node@1.2.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-scaffolder-common@1.2.3-next.0 + +## 1.8.0 + +### Minor Changes + +- ea14eb62a2: Added a set of default Prometheus metrics around scaffolding. See below for a list of metrics and an explanation of their labels: + + - `scaffolder_task_count`: Tracks successful task runs. + + Labels: + + - `template`: The entity ref of the scaffolded template + - `user`: The entity ref of the user that invoked the template run + - `result`: A string describing whether the task ran successfully, failed, or was skipped + + - `scaffolder_task_duration`: a histogram which tracks the duration of a task run + + Labels: + + - `template`: The entity ref of the scaffolded template + - `result`: A boolean describing whether the task ran successfully + + - `scaffolder_step_count`: a count that tracks each step run + + Labels: + + - `template`: The entity ref of the scaffolded template + - `step`: The name of the step that was run + - `result`: A string describing whether the task ran successfully, failed, or was skipped + + - `scaffolder_step_duration`: a histogram which tracks the duration of each step run + + Labels: + + - `template`: The entity ref of the scaffolded template + - `step`: The name of the step that was run + - `result`: A string describing whether the task ran successfully, failed, or was skipped + + You can find a guide for running Prometheus metrics here: https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/prometheus-metrics.md + +- 5921b5ce49: - The GitLab Project ID for the `publish:gitlab:merge-request` action is now passed through the query parameter `project` in the `repoUrl`. It still allows people to not use the `projectid` and use the `repoUrl` with the `owner` and `repo` query parameters instead. This makes it easier to publish to repositories instead of writing the full path to the project. +- 5025d2e8b6: Adds the ability to pass (an optional) array of strings that will be applied to the newly scaffolded repository as topic labels. + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- 969a8444ea: Updated dependency `esbuild` to `^0.15.0`. +- 9ff4ff3745: Implement "Branch protection rules" support for "publish:github" action +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-catalog-backend@1.5.1 + - @backstage/integration@1.4.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/types@1.0.1 + - @backstage/backend-plugin-api@0.1.4 + - @backstage/plugin-catalog-node@1.2.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-scaffolder-common@1.2.2 + +## 1.8.0-next.2 + +### Minor Changes + +- 5025d2e8b6: Adds the ability to pass (an optional) array of strings that will be applied to the newly scaffolded repository as topic labels. + +### Patch Changes + +- 969a8444ea: Updated dependency `esbuild` to `^0.15.0`. +- 9ff4ff3745: Implement "Branch protection rules" support for "publish:github" action +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-plugin-api@0.1.4-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/plugin-catalog-backend@1.5.1-next.1 + - @backstage/plugin-catalog-node@1.2.1-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-scaffolder-common@1.2.2-next.0 + +## 1.8.0-next.1 + +### Minor Changes + +- 5921b5ce49: - The GitLab Project ID for the `publish:gitlab:merge-request` action is now passed through the query parameter `project` in the `repoUrl`. It still allows people to not use the `projectid` and use the `repoUrl` with the `owner` and `repo` query parameters instead. This makes it easier to publish to repositories instead of writing the full path to the project. + ## 1.8.0-next.0 ### Minor Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 512c520128..1b5ea877b8 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -198,6 +198,13 @@ export function createGithubRepoCreateAction(options: { allowMergeCommit?: boolean | undefined; allowAutoMerge?: boolean | undefined; requireCodeOwnerReviews?: boolean | undefined; + bypassPullRequestAllowances?: + | { + users?: string[] | undefined; + teams?: string[] | undefined; + apps?: string[] | undefined; + } + | undefined; requiredStatusCheckContexts?: string[] | undefined; requireBranchesToBeUpToDate?: boolean | undefined; repoVisibility?: 'internal' | 'private' | 'public' | undefined; @@ -236,6 +243,13 @@ export function createGithubRepoPushAction(options: { gitAuthorName?: string | undefined; gitAuthorEmail?: string | undefined; requireCodeOwnerReviews?: boolean | undefined; + bypassPullRequestAllowances?: + | { + users?: string[]; + teams?: string[]; + apps?: string[]; + } + | undefined; requiredStatusCheckContexts?: string[] | undefined; requireBranchesToBeUpToDate?: boolean | undefined; sourcePath?: string | undefined; @@ -366,6 +380,13 @@ export function createPublishGithubAction(options: { allowMergeCommit?: boolean | undefined; allowAutoMerge?: boolean | undefined; sourcePath?: string | undefined; + bypassPullRequestAllowances?: + | { + users?: string[]; + teams?: string[]; + apps?: string[]; + } + | undefined; requireCodeOwnerReviews?: boolean | undefined; requiredStatusCheckContexts?: string[] | undefined; requireBranchesToBeUpToDate?: boolean | undefined; @@ -422,6 +443,7 @@ export function createPublishGitlabAction(options: { gitAuthorName?: string | undefined; gitAuthorEmail?: string | undefined; setUserAsOwner?: boolean | undefined; + topics?: string[] | undefined; }>; // @public @@ -435,7 +457,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'update' | 'create' | 'delete' | undefined; + commitAction?: 'update' | 'delete' | 'create' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index df4597ed04..e814b934ee 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "1.8.0-next.0", + "version": "1.8.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -78,7 +78,7 @@ "vm2": "^3.9.11", "winston": "^3.2.1", "yaml": "^2.0.0", - "zen-observable": "^0.8.15", + "zen-observable": "^0.10.0", "zod": "^3.11.6" }, "devDependencies": { @@ -91,10 +91,10 @@ "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", "@types/zen-observable": "^0.8.0", - "esbuild": "^0.14.1", + "esbuild": "^0.15.0", "jest-when": "^3.1.0", "mock-fs": "^5.1.0", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.3", "yaml": "^2.0.0" }, diff --git a/plugins/scaffolder-backend/scripts/build-nunjucks.js b/plugins/scaffolder-backend/scripts/build-nunjucks.js index b195f96159..8b98ac4305 100755 --- a/plugins/scaffolder-backend/scripts/build-nunjucks.js +++ b/plugins/scaffolder-backend/scripts/build-nunjucks.js @@ -58,7 +58,7 @@ require('esbuild') bundle: true, format: 'cjs', platform: 'node', - target: 'node14', + target: 'node16', banner: { js: NUNJUCKS_LICENSE }, external: ['fsevents'], outfile: path.resolve(__dirname, '../assets/nunjucks.js.txt'), diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 09d7bdcbb2..8a9da38bf2 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -14,14 +14,9 @@ * limitations under the License. */ import { - configServiceRef, createBackendPlugin, - databaseServiceRef, - loggerServiceRef, + coreServices, loggerToWinstonLogger, - permissionsServiceRef, - urlReaderServiceRef, - httpRouterServiceRef, createExtensionPoint, } from '@backstage/backend-plugin-api'; import { ScmIntegrations } from '@backstage/integration'; @@ -86,12 +81,12 @@ export const scaffolderPlugin = createBackendPlugin({ env.registerInit({ deps: { - logger: loggerServiceRef, - config: configServiceRef, - reader: urlReaderServiceRef, - permissions: permissionsServiceRef, - database: databaseServiceRef, - httpRouter: httpRouterServiceRef, + logger: coreServices.logger, + config: coreServices.config, + reader: coreServices.urlReader, + permissions: coreServices.permissions, + database: coreServices.database, + httpRouter: coreServices.httpRouter, catalogClient: catalogServiceRef, }, async init({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts index 9348526b20..760f42eade 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.ts @@ -52,6 +52,11 @@ export function createGithubRepoCreateAction(options: { allowMergeCommit?: boolean; allowAutoMerge?: boolean; requireCodeOwnerReviews?: boolean; + bypassPullRequestAllowances?: { + users?: string[]; + teams?: string[]; + apps?: string[]; + }; requiredStatusCheckContexts?: string[]; requireBranchesToBeUpToDate?: boolean; repoVisibility?: 'private' | 'internal' | 'public'; @@ -85,6 +90,7 @@ export function createGithubRepoCreateAction(options: { homepage: inputProps.homepage, access: inputProps.access, requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, + bypassPullRequestAllowances: inputProps.bypassPullRequestAllowances, requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, requireBranchesToBeUpToDate: inputProps.requireBranchesToBeUpToDate, repoVisibility: inputProps.repoVisibility, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts index b02a475698..1694e37710 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoPush.ts @@ -49,6 +49,13 @@ export function createGithubRepoPushAction(options: { gitAuthorName?: string; gitAuthorEmail?: string; requireCodeOwnerReviews?: boolean; + bypassPullRequestAllowances?: + | { + users?: string[]; + teams?: string[]; + apps?: string[]; + } + | undefined; requiredStatusCheckContexts?: string[]; requireBranchesToBeUpToDate?: boolean; sourcePath?: string; @@ -65,6 +72,7 @@ export function createGithubRepoPushAction(options: { repoUrl: inputProps.repoUrl, requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, + bypassPullRequestAllowances: inputProps.bypassPullRequestAllowances, requireBranchesToBeUpToDate: inputProps.requireBranchesToBeUpToDate, defaultBranch: inputProps.defaultBranch, protectDefaultBranch: inputProps.protectDefaultBranch, @@ -94,6 +102,7 @@ export function createGithubRepoPushAction(options: { gitAuthorName, gitAuthorEmail, requireCodeOwnerReviews = false, + bypassPullRequestAllowances, requiredStatusCheckContexts = [], requireBranchesToBeUpToDate = true, token: providedToken, @@ -131,6 +140,7 @@ export function createGithubRepoPushAction(options: { client, repo, requireCodeOwnerReviews, + bypassPullRequestAllowances, requiredStatusCheckContexts, requireBranchesToBeUpToDate, config, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts index 2264da1dac..fe29378e2e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Config } from '@backstage/config'; import { assertError, InputError } from '@backstage/errors'; import { @@ -124,6 +125,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( topics: string[] | undefined, logger: Logger, ) { + // eslint-disable-next-line testing-library/no-await-sync-query const user = await client.rest.users.getByUsername({ username: owner, }); @@ -247,6 +249,13 @@ export async function initRepoPushAndProtect( client: Octokit, repo: string, requireCodeOwnerReviews: boolean, + bypassPullRequestAllowances: + | { + users?: string[]; + teams?: string[]; + apps?: string[]; + } + | undefined, requiredStatusCheckContexts: string[], requireBranchesToBeUpToDate: boolean, config: Config, @@ -289,6 +298,7 @@ export async function initRepoPushAndProtect( repoName: repo, logger, defaultBranch, + bypassPullRequestAllowances, requireCodeOwnerReviews, requiredStatusCheckContexts, requireBranchesToBeUpToDate, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts index b00d5fa132..e832c3dbfa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/inputProperties.ts @@ -147,6 +147,35 @@ const protectEnforceAdmins = { type: 'boolean', description: `Enforce admins to adhere to default branch protection. The default value is 'true'`, }; + +const bypassPullRequestAllowances = { + title: 'Bypass pull request requirements', + description: + 'Allow specific users, teams, or apps to bypass pull request requirements.', + type: 'object', + additionalProperties: false, + properties: { + apps: { + type: 'array', + items: { + type: 'string', + }, + }, + users: { + type: 'array', + items: { + type: 'string', + }, + }, + teams: { + type: 'array', + items: { + type: 'string', + }, + }, + }, +}; + const gitCommitMessage = { title: 'Git Commit Message', type: 'string', @@ -174,6 +203,7 @@ export { gitCommitMessage }; export { homepage }; export { protectDefaultBranch }; export { protectEnforceAdmins }; +export { bypassPullRequestAllowances }; export { repoUrl }; export { repoVisibility }; export { requireCodeOwnerReviews }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index be77d27a9a..5afe51cc35 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -185,6 +185,11 @@ type BranchProtectionOptions = { logger: Logger; requireCodeOwnerReviews: boolean; requiredStatusCheckContexts?: string[]; + bypassPullRequestAllowances?: { + users?: string[]; + teams?: string[]; + apps?: string[]; + }; requireBranchesToBeUpToDate?: boolean; defaultBranch?: string; enforceAdmins?: boolean; @@ -196,6 +201,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ owner, logger, requireCodeOwnerReviews, + bypassPullRequestAllowances, requiredStatusCheckContexts = [], requireBranchesToBeUpToDate = true, defaultBranch = 'master', @@ -226,6 +232,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ required_pull_request_reviews: { required_approving_review_count: 1, require_code_owner_reviews: requireCodeOwnerReviews, + bypass_pull_request_allowances: bypassPullRequestAllowances, }, }); } catch (e) { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index c1f8e8b5b6..be4cbb1dad 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -29,6 +29,7 @@ import { import * as inputProps from '../github/inputProperties'; import * as outputProps from '../github/outputProperties'; import { parseRepoUrl } from './util'; + /** * Creates a new action that initializes a git repository of the content in the workspace * and publishes it to GitHub. @@ -59,6 +60,13 @@ export function createPublishGithubAction(options: { allowMergeCommit?: boolean; allowAutoMerge?: boolean; sourcePath?: string; + bypassPullRequestAllowances?: + | { + users?: string[]; + teams?: string[]; + apps?: string[]; + } + | undefined; requireCodeOwnerReviews?: boolean; requiredStatusCheckContexts?: string[]; requireBranchesToBeUpToDate?: boolean; @@ -93,6 +101,7 @@ export function createPublishGithubAction(options: { description: inputProps.description, homepage: inputProps.homepage, access: inputProps.access, + bypassPullRequestAllowances: inputProps.bypassPullRequestAllowances, requireCodeOwnerReviews: inputProps.requireCodeOwnerReviews, requiredStatusCheckContexts: inputProps.requiredStatusCheckContexts, requireBranchesToBeUpToDate: inputProps.requireBranchesToBeUpToDate, @@ -129,6 +138,7 @@ export function createPublishGithubAction(options: { homepage, access, requireCodeOwnerReviews = false, + bypassPullRequestAllowances, requiredStatusCheckContexts = [], requireBranchesToBeUpToDate = true, repoVisibility = 'private', @@ -195,6 +205,7 @@ export function createPublishGithubAction(options: { client, repo, requireCodeOwnerReviews, + bypassPullRequestAllowances, requiredStatusCheckContexts, requireBranchesToBeUpToDate, config, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 9fc95b1b5e..53669efd0b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -44,6 +44,7 @@ export function createPublishGitlabAction(options: { gitAuthorName?: string; gitAuthorEmail?: string; setUserAsOwner?: boolean; + topics?: string[]; }>({ id: 'publish:gitlab', description: @@ -99,6 +100,14 @@ export function createPublishGitlabAction(options: { description: 'Set the token user as owner of the newly created repository. Requires a token authorized to do the edit in the integration configuration for the matching host', }, + topics: { + title: 'Topic labels', + description: 'Topic labels to apply on the repository.', + type: 'array', + items: { + type: 'string', + }, + }, }, }, output: { @@ -128,6 +137,7 @@ export function createPublishGitlabAction(options: { gitAuthorName, gitAuthorEmail, setUserAsOwner = false, + topics = [], } = ctx.input; const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); @@ -173,6 +183,7 @@ export function createPublishGitlabAction(options: { namespace_id: targetNamespace, name: repo, visibility: repoVisibility, + ...(topics.length ? { topics } : {}), }); // When setUserAsOwner is true the input token is expected to come from an unprivileged user GitLab diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index 64761f0871..2a71c9f748 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -42,7 +42,7 @@ export const createPublishGitlabMergeRequestAction = (options: { targetPath?: string; token?: string; commitAction?: 'create' | 'delete' | 'update'; - /** @deprecated Use projectPath instead */ + /** @deprecated projectID passed as query parameters in the repoUrl */ projectid?: string; removeSourceBranch?: boolean; assignee?: string; @@ -58,7 +58,7 @@ export const createPublishGitlabMergeRequestAction = (options: { title: 'Repository Location', description: `Accepts the format 'gitlab.com/group_name/project_name' where 'project_name' is the repository name and 'group_name' is a group or username`, }, - /** @deprecated Use projectPath instead */ + /** @deprecated projectID is passed as query parameters in the repoUrl */ projectid: { type: 'string', title: 'projectid', @@ -146,14 +146,12 @@ export const createPublishGitlabMergeRequestAction = (options: { title, token: providedToken, } = ctx.input; - const { host, owner, repo } = parseRepoUrl(repoUrl, integrations); - const projectPath = `${owner}/${repo}`; - if (ctx.input.projectid) { - const deprecationWarning = `Property "projectid" is deprecated and no longer to needed to create a MR`; - ctx.logger.warn(deprecationWarning); - console.warn(deprecationWarning); - } + const { host, owner, repo, project } = parseRepoUrl( + repoUrl, + integrations, + ); + const repoID = project ? project : `${owner}/${repo}`; const integrationConfig = integrations.gitlab.byHost(host); @@ -211,22 +209,19 @@ export const createPublishGitlabMergeRequestAction = (options: { content: file.content.toString('base64'), execute_filemode: file.executable, })); - const projects = await api.Projects.show(projectPath); + + const projects = await api.Projects.show(repoID); const { default_branch: defaultBranch } = projects; try { - await api.Branches.create( - projectPath, - branchName, - String(defaultBranch), - ); + await api.Branches.create(repoID, branchName, String(defaultBranch)); } catch (e) { throw new InputError(`The branch creation failed ${e}`); } try { - await api.Commits.create(projectPath, branchName, title, actions); + await api.Commits.create(repoID, branchName, ctx.input.title, actions); } catch (e) { throw new InputError( `Committing the changes to ${branchName} failed ${e}`, @@ -235,7 +230,7 @@ export const createPublishGitlabMergeRequestAction = (options: { try { const mergeRequestUrl = await api.MergeRequests.create( - projectPath, + repoID, branchName, String(defaultBranch), title, @@ -247,9 +242,8 @@ export const createPublishGitlabMergeRequestAction = (options: { ).then((mergeRequest: { web_url: string }) => { return mergeRequest.web_url; }); - /** @deprecated */ - ctx.output('projectid', projectPath); - ctx.output('projectPath', projectPath); + ctx.output('projectid', repoID); + ctx.output('projectPath', repoID); ctx.output('mergeRequestUrl', mergeRequestUrl); } catch (e) { throw new InputError(`Merge request creation failed${e}`); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts index cc879d4f55..5446109056 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts @@ -15,7 +15,9 @@ */ import path from 'path'; -import { getRepoSourceDirectory, isExecutable } from './util'; +import { getRepoSourceDirectory, isExecutable, parseRepoUrl } from './util'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; describe('getRepoSourceDirectory', () => { it('should return workspace root if no sub folder is given', () => { @@ -85,3 +87,95 @@ describe('isExecutable', () => { expect(isExecutable(0o100640)).toBe(false); }); }); + +describe('parseRepoUrl', () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'www.gitlab.com', + token: 'token', + apiBaseUrl: 'https://api.gitlab.com', + }, + ], + bitbucket: [ + { + host: 'www.bitbucket.net', + apiBaseUrl: 'https://api.hosted.bitbucket.net', + }, + { + host: 'www.bitbucket.org', + username: 'username', + appPassword: 'password', + apiBaseUrl: 'https://api.hosted.bitbucket.org', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + it('should throw an exception if no parameters are passed (gitlab)', () => { + expect(() => parseRepoUrl('www.gitlab.com', integrations)).toThrow( + 'Invalid repo URL passed to publisher: https://www.gitlab.com/, missing owner', + ); + }); + it('should return the project ID in the `project` parameter (gitlab)', () => { + expect( + parseRepoUrl('www.gitlab.com?project=234', integrations), + ).toStrictEqual({ + host: 'www.gitlab.com', + organization: undefined, + owner: undefined, + project: '234', + repo: null, + workspace: undefined, + }); + }); + it('should throw an exception if the repo parameter is missing, but owner parameter is set', () => { + expect(() => + parseRepoUrl('www.gitlab.com?owner=owner', integrations), + ).toThrow( + 'Invalid repo URL passed to publisher: https://www.gitlab.com/?owner=owner, missing repo', + ); + }); + it('should throw an exception if the owner parameter is missing, but repo parameter is set (gitlab)', () => { + expect(() => + parseRepoUrl('www.gitlab.com?repo=repo', integrations), + ).toThrow( + 'Invalid repo URL passed to publisher: https://www.gitlab.com/?repo=repo, missing owner', + ); + }); + it('should return the workspace, project and repo (bitbucket.org)', () => { + expect( + parseRepoUrl( + 'www.bitbucket.org?project=project&workspace=workspace&repo=repo', + integrations, + ), + ).toStrictEqual({ + host: 'www.bitbucket.org', + organization: undefined, + owner: undefined, + project: 'project', + repo: 'repo', + workspace: 'workspace', + }); + }); + it('should return the project and repo (another bitbucket instance)', () => { + expect( + parseRepoUrl('www.bitbucket.net?project=project&repo=repo', integrations), + ).toStrictEqual({ + host: 'www.bitbucket.net', + organization: undefined, + owner: undefined, + project: 'project', + repo: 'repo', + workspace: undefined, + }); + }); + it('should throw an exception if the workspace parameter is missing for bitbucket.org instance (bitbucket.org)', () => { + expect(() => + parseRepoUrl('www.bitbucket.org?project=project&repo=repo', integrations), + ).toThrow( + 'Invalid repo URL passed to publisher: https://www.bitbucket.org/?project=project&repo=repo, missing workspace', + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts index fff3213cb7..61bd704a0b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts @@ -71,32 +71,30 @@ export const parseRepoUrl = ( ); } - if (type === 'bitbucket') { - if (host === 'bitbucket.org') { - if (!workspace) { - throw new InputError( - `Invalid repo URL passed to publisher: ${repoUrl}, missing workspace`, - ); + const repo: string = parsed.searchParams.get('repo')!; + switch (type) { + case 'bitbucket': { + if (host === 'www.bitbucket.org') { + checkRequiredParams(parsed, 'workspace'); } + checkRequiredParams(parsed, 'project', 'repo'); + break; } - if (!project) { - throw new InputError( - `Invalid repo URL passed to publisher: ${repoUrl}, missing project`, - ); + case 'gitlab': { + // project is the projectID, and if defined, owner and repo won't be needed. + if (!project) { + checkRequiredParams(parsed, 'owner', 'repo'); + } + break; } - } else { - if (!owner && type !== 'gerrit') { - throw new InputError( - `Invalid repo URL passed to publisher: ${repoUrl}, missing owner`, - ); + case 'gerrit': { + checkRequiredParams(parsed, 'repo'); + break; + } + default: { + checkRequiredParams(parsed, 'repo', 'owner'); + break; } - } - - const repo = parsed.searchParams.get('repo'); - if (!repo) { - throw new InputError( - `Invalid repo URL passed to publisher: ${repoUrl}, missing repo`, - ); } return { host, owner, repo, organization, workspace, project }; @@ -106,3 +104,26 @@ export const isExecutable = (fileMode: number) => { const res = fileMode & executeBitMask; return res > 0; }; + +/** + * This function checks if the required parameters (given as the `params` argument) are present in the URL + * + * @param repoUrl - the URL for the repository + * @param params - a variadic list of URL query parameter names + * + * @throws + * An InputError exception is thrown if any of the required parameters is not in the URL. + * + * @public + */ +function checkRequiredParams(repoUrl: URL, ...params: string[]) { + for (let i = 0; i < params.length; i++) { + if (!repoUrl.searchParams.get(params[i])) { + throw new InputError( + `Invalid repo URL passed to publisher: ${repoUrl.toString()}, missing ${ + params[i] + }`, + ); + } + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index 421afb2329..5eeb73711e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -50,7 +50,9 @@ describe('StorageTaskBroker', () => { it('should claim a dispatched work item', async () => { const broker = new StorageTaskBroker(storage, logger); await broker.dispatch({ spec: {} as TaskSpec }); - await expect(broker.claim()).resolves.toEqual(expect.any(TaskManager)); + await expect(broker.claim()).resolves.toEqual( + expect.any(TaskManager as any), + ); }); it('should wait for a dispatched work item', async () => { @@ -60,7 +62,7 @@ describe('StorageTaskBroker', () => { await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); await broker.dispatch({ spec: {} as TaskSpec }); - await expect(promise).resolves.toEqual(expect.any(TaskManager)); + await expect(promise).resolves.toEqual(expect.any(TaskManager as any)); }); it('should dispatch multiple items and claim them in order', async () => { @@ -72,9 +74,9 @@ describe('StorageTaskBroker', () => { const taskA = await broker.claim(); const taskB = await broker.claim(); const taskC = await broker.claim(); - await expect(taskA).toEqual(expect.any(TaskManager)); - await expect(taskB).toEqual(expect.any(TaskManager)); - await expect(taskC).toEqual(expect.any(TaskManager)); + await expect(taskA).toEqual(expect.any(TaskManager as any)); + await expect(taskB).toEqual(expect.any(TaskManager as any)); + await expect(taskC).toEqual(expect.any(TaskManager as any)); await expect(taskA.spec.steps[0].id).toBe('a'); await expect(taskB.spec.steps[0].id).toBe('b'); await expect(taskC.spec.steps[0].id).toBe('c'); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index dbd085bf4c..6d21bf4a93 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -23,9 +23,10 @@ import { stringifyEntityRef, UserEntity, } from '@backstage/catalog-model'; -import { Config, JsonObject, JsonValue } from '@backstage/config'; +import { Config } from '@backstage/config'; import { InputError, NotFoundError, stringifyError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; +import { JsonObject, JsonValue } from '@backstage/types'; import { TaskSpec, TemplateEntityV1beta3, diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index b6f8b3e77b..d37b2219d9 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-scaffolder-common +## 1.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## 1.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + +## 1.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + ## 1.2.2-next.0 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 6db049aaed..c84599b228 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "1.2.2-next.0", + "version": "1.2.3-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 709809f5f7..d47bf59283 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,121 @@ # @backstage/plugin-scaffolder +## 1.9.0-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/plugin-permission-react@0.4.8-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-scaffolder-common@1.2.3-next.1 + +## 1.9.0-next.0 + +### Minor Changes + +- ddd1c3308d: Implement Custom Field Explorer to view and play around with available installed custom field extensions +- adb1b01e32: Adds the ability to supply a `transformErrors` function to the `Stepper` for `/next` + +### Patch Changes + +- d4d07cf55e: Enabling the customization of the last step in the scaffolder template. + + To override the content you have to do the next: + + ```typescript jsx + + ``` + +- ef803022f1: Initialize all `formData` in the `Stepper` in `/next` +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- a63e2df559: fixed `headerOptions` not passed to `TemplatePage` component +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-permission-react@0.4.8-next.0 + - @backstage/plugin-scaffolder-common@1.2.3-next.0 + +## 1.8.0 + +### Minor Changes + +- edae17309e: Added props to override default Scaffolder page title, subtitle and pageTitleOverride. + Routes like `rootRouteRef`, `selectedTemplateRouteRef`, `nextRouteRef`, `nextSelectedTemplateRouteRef` were made public and can be used in your app (e.g. in custom TemplateCard component). + +### Patch Changes + +- 580285787d: The `create` and `click` analytics events are now also captured on the "next" version of the component creation page. +- 4830a3569f: Basic analytics instrumentation is now in place: + + - As users make their way through template steps, a `click` event is fired, including the step number. + - After a user clicks "Create" a `create` event is fired, including the name of the software that was just created. The template used at creation is set on the `entityRef` context key. + +- 94b7ca9c6d: Updated to use `@rjsf` packages of version `^5.0.0-beta.12` +- 87840c8c6c: Fixed tiny grammar error in EntityNamePicker. The first letter of the description is now capitalized. +- 3b3fc3cc3c: Fix `formData` not being present in the `next` version +- b2bb48a6f4: Fix the return type for the `createNextScaffodlerFieldExtension` type as before it wasn't a component type for the extension +- f905853ad6: Prefer using `Link` from `@backstage/core-components` rather than material-UI. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/integration-react@1.1.6 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-permission-react@0.4.7 + - @backstage/plugin-scaffolder-common@1.2.2 + +## 1.8.0-next.1 + +### Patch Changes + +- 580285787d: The `create` and `click` analytics events are now also captured on the "next" version of the component creation page. +- 3b3fc3cc3c: Fix `formData` not being present in the `next` version +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-permission-react@0.4.7-next.0 + - @backstage/plugin-scaffolder-common@1.2.2-next.0 + ## 1.8.0-next.0 ### Minor Changes diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 158c75e5bd..39e58a7a42 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -11,6 +11,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import type { ErrorTransformer } from '@rjsf/utils'; import { Extension } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; @@ -35,6 +36,7 @@ import { TaskStep } from '@backstage/plugin-scaffolder-common'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { UIOptionsType } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; +import { z } from 'zod'; // @alpha export function createNextScaffolderFieldExtension< @@ -42,7 +44,7 @@ export function createNextScaffolderFieldExtension< TInputProps extends UIOptionsType = {}, >( options: NextFieldExtensionOptions, -): Extension>; +): Extension>; // @public export function createScaffolderFieldExtension< @@ -57,6 +59,12 @@ export function createScaffolderLayout( options: LayoutOptions, ): Extension>; +// @public +export type CustomFieldExtensionSchema = { + returnValue: JSONSchema7; + uiOptions?: JSONSchema7; +}; + // @public export type CustomFieldValidator = ( data: TFieldReturnValue, @@ -75,36 +83,52 @@ export const EntityNamePickerFieldExtension: FieldExtensionComponent< // @public export const EntityPickerFieldExtension: FieldExtensionComponent< string, - EntityPickerUiOptions + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } +>; + +// @public (undocumented) +export const EntityPickerFieldSchema: FieldSchema< + string, + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } >; // @public -export interface EntityPickerUiOptions { - // (undocumented) - allowArbitraryValues?: boolean; - // (undocumented) - allowedKinds?: string[]; - // (undocumented) - defaultKind?: string; - // (undocumented) - defaultNamespace?: string | false; -} +export type EntityPickerUiOptions = + typeof EntityPickerFieldSchema.uiOptionsType; // @public export const EntityTagsPickerFieldExtension: FieldExtensionComponent< string[], - EntityTagsPickerUiOptions + { + showCounts?: boolean | undefined; + kinds?: string[] | undefined; + helperText?: string | undefined; + } +>; + +// @public (undocumented) +export const EntityTagsPickerFieldSchema: FieldSchema< + string[], + { + showCounts?: boolean | undefined; + kinds?: string[] | undefined; + helperText?: string | undefined; + } >; // @public -export interface EntityTagsPickerUiOptions { - // (undocumented) - helperText?: string; - // (undocumented) - kinds?: string[]; - // (undocumented) - showCounts?: boolean; -} +export type EntityTagsPickerUiOptions = + typeof EntityTagsPickerFieldSchema.uiOptionsType; // @public export type FieldExtensionComponent<_TReturnValue, _TInputProps> = () => null; @@ -130,8 +154,19 @@ export type FieldExtensionOptions< props: FieldExtensionComponentProps, ) => JSX.Element | null; validation?: CustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; +// @public +export interface FieldSchema { + // (undocumented) + readonly schema: CustomFieldExtensionSchema; + // (undocumented) + readonly type: FieldExtensionComponentProps; + // (undocumented) + readonly uiOptionsType: TUiOptions; +} + // @public export type LayoutComponent<_TInputProps> = () => null; @@ -169,6 +204,20 @@ export type LogEvent = { taskId: string; }; +// @public +export function makeFieldSchemaFromZod< + TReturnSchema extends z.ZodType, + TUiOptionsSchema extends z.ZodType = z.ZodType, +>( + returnSchema: TReturnSchema, + uiOptionsSchema?: TUiOptionsSchema, +): FieldSchema< + TReturnSchema extends z.ZodType ? IReturn : never, + TUiOptionsSchema extends z.ZodType + ? IUiOptions + : never +>; + // @alpha export type NextCustomFieldValidator = ( data: TFieldReturnValue, @@ -199,6 +248,7 @@ export type NextFieldExtensionOptions< props: NextFieldExtensionComponentProps, ) => JSX.Element | null; validation?: NextCustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; // @alpha (undocumented) @@ -213,6 +263,7 @@ export type NextRouterProps = { TaskPageComponent?: React_2.ComponentType<{}>; }; groups?: TemplateGroupFilter[]; + transformErrors?: ErrorTransformer; }; // @alpha @@ -228,36 +279,51 @@ export const nextSelectedTemplateRouteRef: SubRouteRef< // @public export const OwnedEntityPickerFieldExtension: FieldExtensionComponent< string, - OwnedEntityPickerUiOptions + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } +>; + +// @public (undocumented) +export const OwnedEntityPickerFieldSchema: FieldSchema< + string, + { + defaultKind?: string | undefined; + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } >; // @public -export interface OwnedEntityPickerUiOptions { - // (undocumented) - allowArbitraryValues?: boolean; - // (undocumented) - allowedKinds?: string[]; - // (undocumented) - defaultKind?: string; - // (undocumented) - defaultNamespace?: string | false; -} +export type OwnedEntityPickerUiOptions = + typeof OwnedEntityPickerFieldSchema.uiOptionsType; // @public export const OwnerPickerFieldExtension: FieldExtensionComponent< string, - OwnerPickerUiOptions + { + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } +>; + +// @public (undocumented) +export const OwnerPickerFieldSchema: FieldSchema< + string, + { + defaultNamespace?: string | false | undefined; + allowedKinds?: string[] | undefined; + allowArbitraryValues?: boolean | undefined; + } >; // @public -export interface OwnerPickerUiOptions { - // (undocumented) - allowArbitraryValues?: boolean; - // (undocumented) - allowedKinds?: string[]; - // (undocumented) - defaultNamespace?: string | false; -} +export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType; // @public export const repoPickerValidation: ( @@ -271,31 +337,70 @@ export const repoPickerValidation: ( // @public export const RepoUrlPickerFieldExtension: FieldExtensionComponent< string, - RepoUrlPickerUiOptions + { + allowedOwners?: string[] | undefined; + allowedOrganizations?: string[] | undefined; + allowedRepos?: string[] | undefined; + allowedHosts?: string[] | undefined; + requestUserCredentials?: + | { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + } + | undefined; + } +>; + +// @public (undocumented) +export const RepoUrlPickerFieldSchema: FieldSchema< + string, + { + allowedOwners?: string[] | undefined; + allowedOrganizations?: string[] | undefined; + allowedRepos?: string[] | undefined; + allowedHosts?: string[] | undefined; + requestUserCredentials?: + | { + additionalScopes?: + | { + azure?: string[] | undefined; + github?: string[] | undefined; + gitlab?: string[] | undefined; + bitbucket?: string[] | undefined; + gerrit?: string[] | undefined; + } + | undefined; + secretsKey: string; + } + | undefined; + } >; // @public -export interface RepoUrlPickerUiOptions { - // (undocumented) - allowedHosts?: string[]; - // (undocumented) - allowedOrganizations?: string[]; - // (undocumented) - allowedOwners?: string[]; - // (undocumented) - allowedRepos?: string[]; - // (undocumented) - requestUserCredentials?: { - secretsKey: string; - additionalScopes?: { - gerrit?: string[]; - github?: string[]; - gitlab?: string[]; - bitbucket?: string[]; - azure?: string[]; - }; - }; -} +export type RepoUrlPickerUiOptions = + typeof RepoUrlPickerFieldSchema.uiOptionsType; + +// @public +export type ReviewStepProps = { + disableButtons: boolean; + formData: JsonObject; + handleBack: () => void; + handleReset: () => void; + handleCreate: () => void; + steps: { + uiSchema: UiSchema; + mergedSchema: JsonObject; + schema: JsonObject; + }[]; +}; // @public (undocumented) export const rootRouteRef: RouteRef; @@ -303,6 +408,7 @@ export const rootRouteRef: RouteRef; // @public export type RouterProps = { components?: { + ReviewStepComponent?: ComponentType; TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta3; diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index cfa69c128a..162b53b4a3 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "1.8.0-next.0", + "version": "1.9.0-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -53,13 +53,13 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@react-hookz/web": "^15.0.0", + "@react-hookz/web": "^20.0.0", "@rjsf/core": "^3.2.1", - "@rjsf/core-v5": "npm:@rjsf/core@^5.0.0-beta.10", + "@rjsf/core-v5": "npm:@rjsf/core@^5.0.0-beta.12", "@rjsf/material-ui": "^3.2.1", - "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@^5.0.0-beta.10", - "@rjsf/utils": "^5.0.0-beta.10", - "@rjsf/validator-ajv8": "^5.0.0-beta.10", + "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@^5.0.0-beta.12", + "@rjsf/utils": "^5.0.0-beta.12", + "@rjsf/validator-ajv8": "^5.0.0-beta.12", "@types/json-schema": "^7.0.9", "@uiw/react-codemirror": "^4.9.3", "classnames": "^2.2.6", @@ -67,14 +67,16 @@ "humanize-duration": "^3.25.1", "immer": "^9.0.1", "json-schema": "^0.4.0", - "json-schema-library": "^7.0.0", + "json-schema-library": "^7.3.9", "lodash": "^4.17.21", "luxon": "^3.0.0", "qs": "^6.9.4", "react-use": "^17.2.4", "use-immer": "^0.7.0", "yaml": "^2.0.0", - "zen-observable": "^0.8.15" + "zen-observable": "^0.10.0", + "zod": "^3.11.6", + "zod-to-json-schema": "^3.18.1" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", @@ -96,7 +98,7 @@ "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", "event-source-polyfill": "1.0.25", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index 40683f01ba..8e2184edf1 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -65,9 +65,9 @@ describe('TemplatePage', () => { }, }, ); - expect(rendered.queryByText('Test title')).toBeInTheDocument(); - expect(rendered.queryByText('example description')).toBeInTheDocument(); - expect(rendered.queryByText('foobar')).toBeInTheDocument(); + expect(rendered.getByText('Test title')).toBeInTheDocument(); + expect(rendered.getByText('example description')).toBeInTheDocument(); + expect(rendered.getByText('foobar')).toBeInTheDocument(); expect(rendered.queryByText('output')).not.toBeInTheDocument(); }); @@ -109,10 +109,10 @@ describe('TemplatePage', () => { }, }, ); - expect(rendered.queryByText('Test title')).toBeInTheDocument(); - expect(rendered.queryByText('example description')).toBeInTheDocument(); - expect(rendered.queryByText('foobar')).toBeInTheDocument(); - expect(rendered.queryByText('Test output')).toBeInTheDocument(); + expect(rendered.getByText('Test title')).toBeInTheDocument(); + expect(rendered.getByText('example description')).toBeInTheDocument(); + expect(rendered.getByText('foobar')).toBeInTheDocument(); + expect(rendered.getByText('Test output')).toBeInTheDocument(); }); it('renders action with oneOf input', async () => { @@ -160,10 +160,10 @@ describe('TemplatePage', () => { }, }, ); - expect(rendered.queryByText('oneOf')).toBeInTheDocument(); - expect(rendered.queryByText('Foo title')).toBeInTheDocument(); - expect(rendered.queryByText('Foo description')).toBeInTheDocument(); - expect(rendered.queryByText('Bar title')).toBeInTheDocument(); - expect(rendered.queryByText('Bar description')).toBeInTheDocument(); + expect(rendered.getByText('oneOf')).toBeInTheDocument(); + expect(rendered.getByText('Foo title')).toBeInTheDocument(); + expect(rendered.getByText('Foo description')).toBeInTheDocument(); + expect(rendered.getByText('Bar title')).toBeInTheDocument(); + expect(rendered.getByText('Bar description')).toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx index 837b87437f..57dde13a43 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx @@ -236,7 +236,7 @@ describe('', () => { ); await act(async () => { - const allButton = await getByText('All'); + const allButton = getByText('All'); fireEvent.click(allButton); }); diff --git a/plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.test.tsx index 63f943d6dd..7c335a0ef7 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/OwnerListPicker.test.tsx @@ -15,7 +15,6 @@ */ import { renderInTestApp } from '@backstage/test-utils'; - import React from 'react'; import { OwnerListPicker } from './OwnerListPicker'; import { fireEvent } from '@testing-library/react'; @@ -29,8 +28,8 @@ describe('', () => { const { getByText } = await renderInTestApp(); - expect(await getByText('Owned')).toBeDefined(); - expect(await getByText('All')).toBeDefined(); + expect(getByText('Owned')).toBeDefined(); + expect(getByText('All')).toBeDefined(); }); it('should call the function on select other item', async () => { @@ -41,7 +40,7 @@ describe('', () => { const { getByText } = await renderInTestApp(); - fireEvent.click(await getByText('All')); + fireEvent.click(getByText('All')); expect(props.onSelectOwner).toHaveBeenCalledWith('all'); }); }); diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.test.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.test.tsx index 21540a4895..3f8e153b65 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.test.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getReviewData } from './MultistepJsonForm'; +import { getReviewData, getUiSchemasFromSteps } from './ReviewStep'; describe('MultistepJsonForm', () => { const formDataMock = { @@ -76,7 +76,8 @@ describe('MultistepJsonForm', () => { ]; test('Fields are defined to be hidden or masked', () => { - const reviewData = getReviewData(formDataMock, stepsMock); + const schemas = getUiSchemasFromSteps(stepsMock); + const reviewData = getReviewData(formDataMock, schemas); expect(reviewData.password).toBe('******'); expect(reviewData.masked).toBe('******'); diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 7afbeb5a97..1fc63b2b61 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -13,41 +13,46 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { JsonObject } from '@backstage/types'; import { - Box, Button, - Paper, Step as StepUI, StepContent, StepLabel, Stepper, Typography, } from '@material-ui/core'; +import { JsonObject } from '@backstage/types'; import { errorApiRef, - useApi, featureFlagsApiRef, useAnalytics, useRouteRefParams, + useApi, } from '@backstage/core-plugin-api'; -import { FormProps, IChangeEvent, UiSchema, withTheme } from '@rjsf/core'; +import { FormProps, IChangeEvent, withTheme } from '@rjsf/core'; +import { UiSchema } from '@rjsf/utils'; import { Theme as MuiTheme } from '@rjsf/material-ui'; -import React, { useState } from 'react'; +import React, { ComponentType, useState } from 'react'; import { transformSchemaToProps } from './schema'; -import { Content, StructuredMetadataTable } from '@backstage/core-components'; import cloneDeep from 'lodash/cloneDeep'; import * as fieldOverrides from './FieldOverrides'; import { LayoutOptions } from '../../layouts'; +import { ReviewStepProps } from '../types'; +import { ReviewStep } from './ReviewStep'; import { selectedTemplateRouteRef } from '../../routes'; +import { extractSchemaFromStep } from '../../next/TemplateWizardPage/Stepper/schema'; const Form = withTheme(MuiTheme); + type Step = { schema: JsonObject; title: string; } & Partial, 'schema'>>; -type Props = { +/** + * The props for a dynamic form of a scaffolder template. + */ +export type MultistepJsonFormProps = { /** * Steps for the form, each contains title and form schema */ @@ -60,62 +65,24 @@ type Props = { fields?: FormProps['fields']; finishButtonLabel?: string; layouts: LayoutOptions[]; + ReviewStepComponent?: ComponentType; }; -export function getUiSchemasFromSteps(steps: Step[]): UiSchema[] { - const uiSchemas: Array = []; - steps.forEach(step => { - const schemaProps = step.schema.properties as JsonObject; - for (const key in schemaProps) { - if (schemaProps.hasOwnProperty(key)) { - const uiSchema = schemaProps[key] as UiSchema; - uiSchema.name = key; - uiSchemas.push(uiSchema); - } - } - }); - return uiSchemas; +export function getSchemasFromSteps(steps: Step[]): { + uiSchema: UiSchema; + mergedSchema: JsonObject; + schema: JsonObject; +}[] { + return steps.map(({ schema }) => ({ + mergedSchema: schema, + ...extractSchemaFromStep(schema), + })); } -export function getReviewData(formData: Record, steps: Step[]) { - const uiSchemas = getUiSchemasFromSteps(steps); - const reviewData: Record = {}; - for (const key in formData) { - if (formData.hasOwnProperty(key)) { - const uiSchema = uiSchemas.find(us => us.name === key); - - if (!uiSchema) { - reviewData[key] = formData[key]; - continue; - } - - if (uiSchema['ui:widget'] === 'password') { - reviewData[key] = '******'; - continue; - } - - if (!uiSchema['ui:backstage'] || !uiSchema['ui:backstage'].review) { - reviewData[key] = formData[key]; - continue; - } - - const review = uiSchema['ui:backstage'].review as JsonObject; - if (review.mask) { - reviewData[key] = review.mask; - continue; - } - - if (!review.show) { - continue; - } - reviewData[key] = formData[key]; - } - } - - return reviewData; -} - -export const MultistepJsonForm = (props: Props) => { +/** + * Creates the dynamic form for a scaffolder template. + */ +export const MultistepJsonForm = (props: MultistepJsonFormProps) => { const { formData, onChange, @@ -123,8 +90,8 @@ export const MultistepJsonForm = (props: Props) => { onFinish, fields, widgets, - finishButtonLabel, layouts, + ReviewStepComponent, } = props; const { templateName } = useRouteRefParams(selectedTemplateRouteRef); const analytics = useAnalytics(); @@ -197,6 +164,8 @@ export const MultistepJsonForm = (props: Props) => { } }; + const ReviewStepElement = ReviewStepComponent ?? ReviewStep; + return ( <> @@ -240,30 +209,14 @@ export const MultistepJsonForm = (props: Props) => { })} {activeStep === steps.length && ( - - - Review and create - - - - - - - + )} ); diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/ReviewStep.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/ReviewStep.tsx new file mode 100644 index 0000000000..d78d726869 --- /dev/null +++ b/plugins/scaffolder/src/components/MultistepJsonForm/ReviewStep.tsx @@ -0,0 +1,121 @@ +/* + * 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 { Box, Button, Paper, Typography } from '@material-ui/core'; +import React from 'react'; +import { Content, StructuredMetadataTable } from '@backstage/core-components'; +import { UiSchema } from '@rjsf/core'; +import { JsonObject } from '@backstage/types'; +import { ReviewStepProps } from '../types'; + +export function getReviewData( + formData: Record, + uiSchemas: UiSchema[], +) { + const reviewData: Record = {}; + for (const key in formData) { + if (formData.hasOwnProperty(key)) { + const uiSchema = uiSchemas.find(us => us.name === key); + + if (!uiSchema) { + reviewData[key] = formData[key]; + continue; + } + + if (uiSchema['ui:widget'] === 'password') { + reviewData[key] = '******'; + continue; + } + + if (!uiSchema['ui:backstage'] || !uiSchema['ui:backstage'].review) { + reviewData[key] = formData[key]; + continue; + } + + const review = uiSchema['ui:backstage'].review as JsonObject; + if (review.mask) { + reviewData[key] = review.mask; + continue; + } + + if (!review.show) { + continue; + } + reviewData[key] = formData[key]; + } + } + + return reviewData; +} + +export function getUiSchemasFromSteps( + steps: { + schema: JsonObject; + }[], +): UiSchema[] { + const uiSchemas: Array = []; + steps.forEach(step => { + const schemaProps = step.schema.properties as JsonObject; + for (const key in schemaProps) { + if (schemaProps.hasOwnProperty(key)) { + const uiSchema = schemaProps[key] as UiSchema; + uiSchema.name = key; + uiSchemas.push(uiSchema); + } + } + }); + return uiSchemas; +} + +/** + * The component displaying the Last Step in scaffolder template form. + * Which represents the summary of the input provided by the end user. + */ +export const ReviewStep = (props: ReviewStepProps) => { + const { + disableButtons, + formData, + handleBack, + handleCreate, + handleReset, + steps, + } = props; + return ( + + + Review and create + + + + + + + + ); +}; diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 097eddf6cd..d35bcbc67a 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -15,7 +15,7 @@ */ import React, { ComponentType, useEffect } from 'react'; -import { Routes, Route, useOutlet, Navigate } from 'react-router'; +import { Navigate, Route, Routes, useOutlet } from 'react-router'; import { Entity } from '@backstage/catalog-model'; import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { ScaffolderPage } from './ScaffolderPage'; @@ -26,10 +26,10 @@ import { SecretsContextProvider } from './secrets/SecretsContext'; import { TemplateEditorPage } from './TemplateEditorPage'; import { - FieldExtensionOptions, - FIELD_EXTENSION_WRAPPER_KEY, - FIELD_EXTENSION_KEY, DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS, + FIELD_EXTENSION_KEY, + FIELD_EXTENSION_WRAPPER_KEY, + FieldExtensionOptions, } from '../extensions'; import { useElementFilter, @@ -46,6 +46,7 @@ import { } from '../routes'; import { ListTasksPage } from './ListTasksPage'; import { LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts'; +import { ReviewStepProps } from './types'; /** * The props for the entrypoint `ScaffolderPage` component the plugin. @@ -53,6 +54,7 @@ import { LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts'; */ export type RouterProps = { components?: { + ReviewStepComponent?: ComponentType; TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta3 }> | undefined; @@ -87,7 +89,8 @@ export type RouterProps = { export const Router = (props: RouterProps) => { const { groups, components = {}, defaultPreviewTemplate } = props; - const { TemplateCardComponent, TaskPageComponent } = components; + const { ReviewStepComponent, TemplateCardComponent, TaskPageComponent } = + components; const outlet = useOutlet(); const TaskPageElement = TaskPageComponent ?? TaskPage; @@ -161,8 +164,10 @@ export const Router = (props: RouterProps) => { element={ } diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx index 2d07416025..02869bb6da 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPageContextMenu.test.tsx @@ -45,7 +45,7 @@ describe('ScaffolderPageContextMenu', () => { await userEvent.click(screen.getByTestId('container').firstElementChild!); - expect(screen.queryByText('Template Editor')).toBeInTheDocument(); + expect(screen.getByText('Template Editor')).toBeInTheDocument(); expect(screen.queryByText('Installed Actions')).not.toBeInTheDocument(); }); @@ -62,7 +62,7 @@ describe('ScaffolderPageContextMenu', () => { await userEvent.click(screen.getByTestId('container').firstElementChild!); expect(screen.queryByText('Template Editor')).not.toBeInTheDocument(); - expect(screen.queryByText('Installed Actions')).toBeInTheDocument(); + expect(screen.getByText('Installed Actions')).toBeInTheDocument(); }); it('renders all options', async () => { @@ -77,7 +77,7 @@ describe('ScaffolderPageContextMenu', () => { await userEvent.click(screen.getByTestId('container').firstElementChild!); - expect(screen.queryByText('Template Editor')).toBeInTheDocument(); - expect(screen.queryByText('Installed Actions')).toBeInTheDocument(); + expect(screen.getByText('Template Editor')).toBeInTheDocument(); + expect(screen.getByText('Installed Actions')).toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/components/TaskPage/IconLink.test.tsx b/plugins/scaffolder/src/components/TaskPage/IconLink.test.tsx index 9c1b021071..f7ce850cd8 100644 --- a/plugins/scaffolder/src/components/TaskPage/IconLink.test.tsx +++ b/plugins/scaffolder/src/components/TaskPage/IconLink.test.tsx @@ -33,6 +33,6 @@ describe('IconLink', () => { , ); - expect(rendered.queryByText('I am Link')).toBeInTheDocument(); + expect(rendered.getByText('I am Link')).toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx new file mode 100644 index 0000000000..3510968acd --- /dev/null +++ b/plugins/scaffolder/src/components/TemplateEditorPage/CustomFieldExplorer.tsx @@ -0,0 +1,201 @@ +/* + * 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 { StreamLanguage } from '@codemirror/language'; +import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; +import { + Button, + Card, + CardContent, + CardHeader, + FormControl, + IconButton, + InputLabel, + makeStyles, + MenuItem, + Select, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import { withTheme } from '@rjsf/core'; +import { Theme as MuiTheme } from '@rjsf/material-ui'; +import CodeMirror from '@uiw/react-codemirror'; +import React, { useCallback, useMemo, useState } from 'react'; +import yaml from 'yaml'; +import { FieldExtensionOptions } from '../../extensions'; +import * as fieldOverrides from '../MultistepJsonForm/FieldOverrides'; +import { TemplateEditorForm } from './TemplateEditorForm'; + +const Form = withTheme(MuiTheme); + +const useStyles = makeStyles(theme => ({ + root: { + gridArea: 'pageContent', + display: 'grid', + gridTemplateAreas: ` + "controls controls" + "fieldForm preview" + `, + gridTemplateRows: 'auto 1fr', + gridTemplateColumns: '1fr 1fr', + }, + controls: { + gridArea: 'controls', + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + margin: theme.spacing(1), + }, + fieldForm: { + gridArea: 'fieldForm', + }, + preview: { + gridArea: 'preview', + }, +})); + +export const CustomFieldExplorer = ({ + customFieldExtensions = [], + onClose, +}: { + customFieldExtensions?: FieldExtensionOptions[]; + onClose?: () => void; +}) => { + const classes = useStyles(); + const fieldOptions = customFieldExtensions.filter(field => !!field.schema); + const [selectedField, setSelectedField] = useState(fieldOptions[0]); + const [fieldFormState, setFieldFormState] = useState({}); + const [formState, setFormState] = useState({}); + const [refreshKey, setRefreshKey] = useState(Date.now()); + const sampleFieldTemplate = useMemo( + () => + yaml.stringify({ + parameters: [ + { + title: `${selectedField.name} Example`, + properties: { + [selectedField.name]: { + type: selectedField.schema?.returnValue?.type, + 'ui:field': selectedField.name, + 'ui:options': fieldFormState, + }, + }, + }, + ], + }), + [fieldFormState, selectedField], + ); + + const fieldComponents = useMemo(() => { + return Object.fromEntries( + customFieldExtensions.map(({ name, component }) => [name, component]), + ); + }, [customFieldExtensions]); + + const handleSelectionChange = useCallback( + selection => { + setSelectedField(selection); + setFieldFormState({}); + setFormState({}); + }, + [setFieldFormState, setFormState, setSelectedField], + ); + + const handleFieldConfigChange = useCallback( + state => { + setFieldFormState(state); + setFormState({}); + // Force TemplateEditorForm to re-render since some fields + // may not be responsive to ui:option changes + setRefreshKey(Date.now()); + }, + [setFieldFormState, setRefreshKey], + ); + + return ( +
+
+ + + Choose Custom Field Extension + + + + + + + +
+
+ + + +
handleFieldConfigChange(e.formData)} + schema={selectedField.schema?.uiOptions || {}} + > + + +
+
+
+
+ + + + + + + null} + /> +
+
+ ); +}; diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.test.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.test.tsx index 184118914a..36a62f1b8a 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.test.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.test.tsx @@ -25,7 +25,7 @@ describe('base64EncodeContent', () => { it('encodes text files', () => { expect(base64EncodeContent('abc')).toBe('YWJj'); expect(base64EncodeContent('abc'.repeat(1000000))).toBe( - btoa(''), + window.btoa(''), ); }); @@ -38,7 +38,7 @@ describe('base64EncodeContent', () => { ); // Triggers size check expect(base64EncodeContent('😅'.repeat(1000000))).toBe( - btoa(''), + window.btoa(''), ); }); }); diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.tsx index b592e3cecd..3534b62c39 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunContext.tsx @@ -59,11 +59,11 @@ interface DryRunProviderProps { export function base64EncodeContent(content: string): string { if (content.length > MAX_CONTENT_SIZE) { - return btoa(''); + return window.btoa(''); } try { - return btoa(content); + return window.btoa(content); } catch { const decoder = new TextEncoder(); const buffer = decoder.encode(content); @@ -74,7 +74,7 @@ export function base64EncodeContent(content: string): string { String.fromCharCode(...buffer.slice(offset, offset + CHUNK_SIZE)), ); } - return btoa(chunks.join('')); + return window.btoa(chunks.join('')); } } diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsList.test.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsList.test.tsx index 65eb59c84a..417c23c483 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsList.test.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsList.test.tsx @@ -70,7 +70,7 @@ describe('DryRunResultsList', () => { , ); - expect(screen.queryByText('Result 1')).toBeInTheDocument(); + expect(screen.getByText('Result 1')).toBeInTheDocument(); expect(screen.queryByText('Result 2')).not.toBeInTheDocument(); await act(async () => { @@ -84,12 +84,12 @@ describe('DryRunResultsList', () => { ); }); - expect(screen.queryByText('Result 1')).toBeInTheDocument(); - expect(screen.queryByText('Result 2')).toBeInTheDocument(); + expect(screen.getByText('Result 1')).toBeInTheDocument(); + expect(screen.getByText('Result 2')).toBeInTheDocument(); await userEvent.click(screen.getAllByLabelText('delete')[0]); expect(screen.queryByText('Result 1')).not.toBeInTheDocument(); - expect(screen.queryByText('Result 2')).toBeInTheDocument(); + expect(screen.getByText('Result 2')).toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsView.test.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsView.test.tsx index bc6b5d8f6f..c709adfd68 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsView.test.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/DryRunResults/DryRunResultsView.test.tsx @@ -58,7 +58,7 @@ describe('DryRunResultsView', () => { directoryContents: [ { path: 'foo.txt', - base64Content: btoa('Foo Content'), + base64Content: window.btoa('Foo Content'), executable: false, }, ], diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx index dc411ee965..61e6c46964 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorIntro.tsx @@ -44,7 +44,7 @@ const useStyles = makeStyles(theme => ({ interface EditorIntroProps { style?: JSX.IntrinsicElements['div']['style']; - onSelect?: (option: 'local' | 'form') => void; + onSelect?: (option: 'local' | 'form' | 'field-explorer') => void; } export function TemplateEditorIntro(props: EditorIntroProps) { @@ -104,6 +104,22 @@ export function TemplateEditorIntro(props: EditorIntroProps) { ); + const cardFieldExplorer = ( + + props.onSelect?.('field-explorer')}> + + + Custom Field Explorer + + + View and play around with available installed custom field + extensions. + + + + + ); + return (
@@ -121,6 +137,7 @@ export function TemplateEditorIntro(props: EditorIntroProps) { {supportsLoad && cardLoadLocal} {cardFormEditor} {!supportsLoad && cardLoadLocal} + {cardFieldExplorer}
); diff --git a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx index b539b6403b..8847b85373 100644 --- a/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx +++ b/plugins/scaffolder/src/components/TemplateEditorPage/TemplateEditorPage.tsx @@ -19,6 +19,7 @@ import { TemplateDirectoryAccess, WebFileSystemAccess, } from '../../lib/filesystem'; +import { CustomFieldExplorer } from './CustomFieldExplorer'; import { TemplateEditorIntro } from './TemplateEditorIntro'; import { TemplateEditor } from './TemplateEditor'; import { TemplateFormPreviewer } from './TemplateFormPreviewer'; @@ -32,6 +33,9 @@ type Selection = } | { type: 'form'; + } + | { + type: 'field-explorer'; }; interface TemplateEditorPageProps { @@ -62,6 +66,13 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) { layouts={props.layouts} /> ); + } else if (selection?.type === 'field-explorer') { + content = ( + setSelection(undefined)} + /> + ); } else { content = ( @@ -73,6 +84,8 @@ export function TemplateEditorPage(props: TemplateEditorPageProps) { .catch(() => {}); } else if (option === 'form') { setSelection({ type: 'form' }); + } else if (option === 'field-explorer') { + setSelection({ type: 'field-explorer' }); } }} /> diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index a48644e72d..5b16c21364 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { MockAnalyticsApi, renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; -import { act, fireEvent, within } from '@testing-library/react'; +import { act, fireEvent, screen, within } from '@testing-library/react'; import React from 'react'; import { Route, Routes } from 'react-router'; import { scaffolderApiRef } from '../../api'; @@ -30,7 +31,6 @@ import { FeatureFlagsApi, analyticsApiRef, } from '@backstage/core-plugin-api'; - import { ApiProvider } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; @@ -134,8 +134,8 @@ describe('TemplatePage', () => { }, ); - expect(rendered.queryByText('Create a New Component')).toBeInTheDocument(); - expect(rendered.queryByText('React SSR Template')).toBeInTheDocument(); + expect(rendered.getByText('Create a New Component')).toBeInTheDocument(); + expect(rendered.getByText('React SSR Template')).toBeInTheDocument(); }); it('renders spinner while loading', async () => { @@ -155,8 +155,8 @@ describe('TemplatePage', () => { }, ); - expect(rendered.queryByText('Create a New Component')).toBeInTheDocument(); - expect(rendered.queryByTestId('loading-progress')).toBeInTheDocument(); + expect(rendered.getByText('Create a New Component')).toBeInTheDocument(); + expect(rendered.getByTestId('loading-progress')).toBeInTheDocument(); await act(async () => { resolve!({ @@ -247,7 +247,7 @@ describe('TemplatePage', () => { expect( rendered.queryByText('Create a New Component'), ).not.toBeInTheDocument(); - expect(rendered.queryByText('This is root')).toBeInTheDocument(); + expect(rendered.getByText('This is root')).toBeInTheDocument(); }); it('display template with oneOf', async () => { @@ -329,7 +329,7 @@ describe('TemplatePage', () => { schemaMockValue, ); - const { queryByText } = await renderInTestApp( + await renderInTestApp( , @@ -340,9 +340,9 @@ describe('TemplatePage', () => { }, ); - expect(await queryByText('Name')).not.toBeInTheDocument(); - expect(await queryByText('Description')).toBeInTheDocument(); - expect(await queryByText('Owner')).toBeInTheDocument(); - expect(await queryByText('Send data')).toBeInTheDocument(); + expect(screen.queryByText('Name')).not.toBeInTheDocument(); + expect(screen.getByText('Description')).toBeInTheDocument(); + expect(screen.getByText('Owner')).toBeInTheDocument(); + expect(screen.getByText('Send data')).toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 87678d2a83..cfe7992ab1 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -16,7 +16,7 @@ import { LinearProgress } from '@material-ui/core'; import { IChangeEvent } from '@rjsf/core'; import qs from 'qs'; -import React, { useCallback, useContext, useState } from 'react'; +import React, { ComponentType, useCallback, useContext, useState } from 'react'; import { Navigate, useNavigate } from 'react-router'; import useAsync from 'react-use/lib/useAsync'; import { scaffolderApiRef } from '../../api'; @@ -41,6 +41,7 @@ import { } from '@backstage/core-plugin-api'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { LayoutOptions } from '../../layouts'; +import { ReviewStepProps } from '../types'; const useTemplateParameterSchema = (templateRef: string) => { const scaffolderApi = useApi(scaffolderApiRef); @@ -51,13 +52,23 @@ const useTemplateParameterSchema = (templateRef: string) => { return { schema: value, loading, error }; }; -export const TemplatePage = ({ - customFieldExtensions = [], - layouts = [], -}: { +type Props = { + ReviewStepComponent?: ComponentType; customFieldExtensions?: FieldExtensionOptions[]; layouts?: LayoutOptions[]; -}) => { + headerOptions?: { + pageTitleOverride?: string; + title?: string; + subtitle?: string; + }; +}; + +export const TemplatePage = ({ + ReviewStepComponent, + customFieldExtensions = [], + layouts = [], + headerOptions, +}: Props) => { const apiHolder = useApiHolder(); const secretsContext = useContext(SecretsContext); const errorApi = useApi(errorApiRef); @@ -136,6 +147,7 @@ export const TemplatePage = ({ pageTitleOverride="Create a New Component" title="Create a New Component" subtitle="Create new software components using standard templates" + {...headerOptions} /> {loading && } @@ -146,6 +158,7 @@ export const TemplatePage = ({ titleTypographyProps={{ component: 'h2' }} > , -) => { +export const EntityNamePicker = (props: EntityNamePickerProps) => { const { onChange, required, diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts new file mode 100644 index 0000000000..27e33befb5 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/schema.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; + +const EntityNamePickerFieldSchema = makeFieldSchemaFromZod(z.string()); + +export const EntityNamePickerSchema = EntityNamePickerFieldSchema.schema; + +export type EntityNamePickerProps = typeof EntityNamePickerFieldSchema.type; diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts index 7a40a460ba..d116bef228 100644 --- a/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts @@ -23,7 +23,7 @@ export const entityNamePickerValidation = ( ) => { if (!KubernetesValidatorFunctions.isValidObjectName(value)) { validation.addError( - 'must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.', + 'Must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.', ); } }; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index e7883a4721..bc78e00576 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -23,20 +23,9 @@ import FormControl from '@material-ui/core/FormControl'; import Autocomplete from '@material-ui/lab/Autocomplete'; import React, { useCallback, useEffect } from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { FieldExtensionComponentProps } from '../../../extensions'; +import { EntityPickerProps } from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `EntityPicker` field extension. - * - * @public - */ -export interface EntityPickerUiOptions { - allowedKinds?: string[]; - defaultKind?: string; - allowArbitraryValues?: boolean; - defaultNamespace?: string | false; -} +export { EntityPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `EntityPicker` @@ -44,9 +33,7 @@ export interface EntityPickerUiOptions { * * @public */ -export const EntityPicker = ( - props: FieldExtensionComponentProps, -) => { +export const EntityPicker = (props: EntityPickerProps) => { const { onChange, schema: { title = 'Entity', description = 'An entity from the catalog' }, diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts index 891b5bef16..b8df596c98 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityPicker/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { EntityPickerUiOptions } from './EntityPicker'; +export { EntityPickerFieldSchema, type EntityPickerUiOptions } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts new file mode 100644 index 0000000000..9236457fc0 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts @@ -0,0 +1,59 @@ +/* + * 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 { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; + +/** + * @public + */ +export const EntityPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), + z.object({ + allowedKinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive options from'), + defaultKind: z + .string() + .optional() + .describe( + 'The default entity kind. Options of this kind will not be prefixed.', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), + }), +); + +/** + * The input props that can be specified under `ui:options` for the + * `EntityPicker` field extension. + * + * @public + */ +export type EntityPickerUiOptions = + typeof EntityPickerFieldSchema.uiOptionsType; + +export type EntityPickerProps = typeof EntityPickerFieldSchema.type; + +export const EntityPickerSchema = EntityPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx index c0e3aab436..549ccdb8aa 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx @@ -22,19 +22,9 @@ import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { FormControl, TextField } from '@material-ui/core'; import { Autocomplete } from '@material-ui/lab'; -import { FieldExtensionComponentProps } from '../../../extensions'; +import { EntityTagsPickerProps } from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `EntityTagsPicker` field extension. - * - * @public - */ -export interface EntityTagsPickerUiOptions { - kinds?: string[]; - showCounts?: boolean; - helperText?: string; -} +export { EntityTagsPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `EntityTagsPicker` @@ -42,9 +32,7 @@ export interface EntityTagsPickerUiOptions { * * @public */ -export const EntityTagsPicker = ( - props: FieldExtensionComponentProps, -) => { +export const EntityTagsPicker = (props: EntityTagsPickerProps) => { const { formData, onChange, uiSchema } = props; const catalogApi = useApi(catalogApiRef); const [tagOptions, setTagOptions] = useState([]); diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts index 9ff6e553a6..52bad9a80d 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { EntityTagsPickerUiOptions } from './EntityTagsPicker'; +export { + EntityTagsPickerFieldSchema, + type EntityTagsPickerUiOptions, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts new file mode 100644 index 0000000000..6677d16e98 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/schema.ts @@ -0,0 +1,48 @@ +/* + * 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 { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; + +/** + * @public + */ +export const EntityTagsPickerFieldSchema = makeFieldSchemaFromZod( + z.array(z.string()), + z.object({ + kinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive tags from'), + showCounts: z + .boolean() + .optional() + .describe('Whether to show usage counts per tag'), + helperText: z.string().optional().describe('Helper text to display'), + }), +); + +export const EntityTagsPickerSchema = EntityTagsPickerFieldSchema.schema; + +export type EntityTagsPickerProps = typeof EntityTagsPickerFieldSchema.type; + +/** + * The input props that can be specified under `ui:options` for the + * `EntityTagsPicker` field extension. + * + * @public + */ +export type EntityTagsPickerUiOptions = + typeof EntityTagsPickerFieldSchema.uiOptionsType; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx index 2446883a86..01a174a6a3 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/OwnedEntityPicker.tsx @@ -26,20 +26,9 @@ import Autocomplete from '@material-ui/lab/Autocomplete'; import React, { useMemo } from 'react'; import useAsync from 'react-use/lib/useAsync'; -import { FieldExtensionComponentProps } from '../../../extensions'; +import { OwnedEntityPickerProps } from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `OwnedEntityPicker` field extension. - * - * @public - */ -export interface OwnedEntityPickerUiOptions { - allowedKinds?: string[]; - defaultKind?: string; - allowArbitraryValues?: boolean; - defaultNamespace?: string | false; -} +export { OwnedEntityPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `OwnedEntityPicker` @@ -47,9 +36,7 @@ export interface OwnedEntityPickerUiOptions { * * @public */ -export const OwnedEntityPicker = ( - props: FieldExtensionComponentProps, -) => { +export const OwnedEntityPicker = (props: OwnedEntityPickerProps) => { const { onChange, schema: { title = 'Entity', description = 'An entity from the catalog' }, diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts index 2988ba8cdc..985dae1d3c 100644 --- a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts @@ -13,4 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { OwnedEntityPickerUiOptions } from './OwnedEntityPicker'; +export { + OwnedEntityPickerFieldSchema, + type OwnedEntityPickerUiOptions, +} from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts new file mode 100644 index 0000000000..4190b89f95 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/schema.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; + +/** + * @public + */ +export const OwnedEntityPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), + z.object({ + allowedKinds: z + .array(z.string()) + .optional() + .describe('List of kinds of entities to derive options from'), + defaultKind: z + .string() + .optional() + .describe( + 'The default entity kind. Options of this kind will not be prefixed.', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), + }), +); + +/** + * The input props that can be specified under `ui:options` for the + * `OwnedEntityPicker` field extension. + * + * @public + */ +export type OwnedEntityPickerUiOptions = + typeof OwnedEntityPickerFieldSchema.uiOptionsType; + +export type OwnedEntityPickerProps = typeof OwnedEntityPickerFieldSchema.type; + +export const OwnedEntityPickerSchema = OwnedEntityPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx index 3de2b12bd0..1c4c356906 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.tsx @@ -15,19 +15,9 @@ */ import React from 'react'; import { EntityPicker } from '../EntityPicker/EntityPicker'; -import { FieldExtensionComponentProps } from '../../../extensions'; +import { OwnerPickerProps } from './schema'; -/** - * The input props that can be specified under `ui:options` for the - * `OwnerPicker` field extension. - * - * @public - */ -export interface OwnerPickerUiOptions { - allowedKinds?: string[]; - allowArbitraryValues?: boolean; - defaultNamespace?: string | false; -} +export { OwnerPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `OwnerPicker` @@ -35,9 +25,7 @@ export interface OwnerPickerUiOptions { * * @public */ -export const OwnerPicker = ( - props: FieldExtensionComponentProps, -) => { +export const OwnerPicker = (props: OwnerPickerProps) => { const { schema: { title = 'Owner', description = 'The owner of the component' }, uiSchema, diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts index aa26024b5b..9d94650e04 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { OwnerPickerUiOptions } from './OwnerPicker'; +export { OwnerPickerFieldSchema, type OwnerPickerUiOptions } from './schema'; diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts new file mode 100644 index 0000000000..56edef1343 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/schema.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; + +/** + * @public + */ +export const OwnerPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), + z.object({ + allowedKinds: z + .array(z.string()) + .default(['Group', 'User']) + .optional() + .describe( + 'List of kinds of entities to derive options from. Defaults to Group and User', + ), + allowArbitraryValues: z + .boolean() + .optional() + .describe('Whether to allow arbitrary user input. Defaults to true'), + defaultNamespace: z + .union([z.string(), z.literal(false)]) + .optional() + .describe( + 'The default namespace. Options with this namespace will not be prefixed.', + ), + }), +); + +/** + * The input props that can be specified under `ui:options` for the + * `OwnerPicker` field extension. + * + * @public + */ +export type OwnerPickerUiOptions = typeof OwnerPickerFieldSchema.uiOptionsType; + +export type OwnerPickerProps = typeof OwnerPickerFieldSchema.type; + +export const OwnerPickerSchema = OwnerPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 451df857e5..827eff6ea2 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -24,36 +24,15 @@ import { GitlabRepoPicker } from './GitlabRepoPicker'; import { AzureRepoPicker } from './AzureRepoPicker'; import { BitbucketRepoPicker } from './BitbucketRepoPicker'; import { GerritRepoPicker } from './GerritRepoPicker'; -import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; import { RepoUrlPickerRepoName } from './RepoUrlPickerRepoName'; import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; +import { RepoUrlPickerProps } from './schema'; import { RepoUrlPickerState } from './types'; import useDebounce from 'react-use/lib/useDebounce'; import { useTemplateSecrets } from '../../secrets'; -/** - * The input props that can be specified under `ui:options` for the - * `RepoUrlPicker` field extension. - * - * @public - */ -export interface RepoUrlPickerUiOptions { - allowedHosts?: string[]; - allowedOrganizations?: string[]; - allowedOwners?: string[]; - allowedRepos?: string[]; - requestUserCredentials?: { - secretsKey: string; - additionalScopes?: { - gerrit?: string[]; - github?: string[]; - gitlab?: string[]; - bitbucket?: string[]; - azure?: string[]; - }; - }; -} +export { RepoUrlPickerSchema } from './schema'; /** * The underlying component that is rendered in the form for the `RepoUrlPicker` @@ -61,9 +40,7 @@ export interface RepoUrlPickerUiOptions { * * @public */ -export const RepoUrlPicker = ( - props: FieldExtensionComponentProps, -) => { +export const RepoUrlPicker = (props: RepoUrlPickerProps) => { const { uiSchema, onChange, rawErrors, formData } = props; const [state, setState] = useState( parseRepoPickerUrl(formData), diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts index c5f596a786..33e5ef1715 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/index.ts @@ -13,5 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type { RepoUrlPickerUiOptions } from './RepoUrlPicker'; +export { + RepoUrlPickerFieldSchema, + type RepoUrlPickerUiOptions, +} from './schema'; export { repoPickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts new file mode 100644 index 0000000000..8f8674a0a7 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/schema.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { z } from 'zod'; +import { makeFieldSchemaFromZod } from '../utils'; + +/** + * @public + */ +export const RepoUrlPickerFieldSchema = makeFieldSchemaFromZod( + z.string(), + z.object({ + allowedHosts: z + .array(z.string()) + .optional() + .describe('List of allowed SCM platform hosts'), + allowedOrganizations: z + .array(z.string()) + .optional() + .describe('List of allowed organizations in the given SCM platform'), + allowedOwners: z + .array(z.string()) + .optional() + .describe('List of allowed owners in the given SCM platform'), + allowedRepos: z + .array(z.string()) + .optional() + .describe('List of allowed repos in the given SCM platform'), + requestUserCredentials: z + .object({ + secretsKey: z + .string() + .describe( + 'Key used within the template secrets context to store the credential', + ), + additionalScopes: z + .object({ + 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 + * `RepoUrlPicker` field extension. + * + * @public + */ +export type RepoUrlPickerUiOptions = + typeof RepoUrlPickerFieldSchema.uiOptionsType; + +export type RepoUrlPickerProps = typeof RepoUrlPickerFieldSchema.type; + +// 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 RepoUrlPickerSchema = RepoUrlPickerFieldSchema.schema; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index 6358934025..8d86f601ce 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -18,3 +18,4 @@ export * from './OwnerPicker'; export * from './RepoUrlPicker'; export * from './OwnedEntityPicker'; export * from './EntityTagsPicker'; +export { type FieldSchema, makeFieldSchemaFromZod } from './utils'; diff --git a/plugins/scaffolder/src/components/fields/utils.ts b/plugins/scaffolder/src/components/fields/utils.ts new file mode 100644 index 0000000000..2d3e2aab1e --- /dev/null +++ b/plugins/scaffolder/src/components/fields/utils.ts @@ -0,0 +1,62 @@ +/* + * 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 { JSONSchema7 } from 'json-schema'; +import { z } from 'zod'; +import zodToJsonSchema from 'zod-to-json-schema'; +import { + CustomFieldExtensionSchema, + FieldExtensionComponentProps, +} from '../../extensions'; + +/** + * @public + * FieldSchema encapsulates a JSONSchema7 along with the + * matching FieldExtensionComponentProps type for a field extension. + */ +export interface FieldSchema { + readonly schema: CustomFieldExtensionSchema; + readonly type: FieldExtensionComponentProps; + readonly uiOptionsType: TUiOptions; +} + +/** + * @public + * Utility function to convert zod return and UI options schemas to a + * CustomFieldExtensionSchema with FieldExtensionComponentProps type inference + */ +export function makeFieldSchemaFromZod< + TReturnSchema extends z.ZodType, + TUiOptionsSchema extends z.ZodType = z.ZodType, +>( + returnSchema: TReturnSchema, + uiOptionsSchema?: TUiOptionsSchema, +): FieldSchema< + TReturnSchema extends z.ZodType ? IReturn : never, + TUiOptionsSchema extends z.ZodType + ? IUiOptions + : never +> { + return { + schema: { + returnValue: zodToJsonSchema(returnSchema) as JSONSchema7, + uiOptions: uiOptionsSchema + ? (zodToJsonSchema(uiOptionsSchema) as JSONSchema7) + : undefined, + }, + type: null as any, + uiOptionsType: null as any, + }; +} diff --git a/plugins/scaffolder/src/components/index.ts b/plugins/scaffolder/src/components/index.ts index d0442cb906..23f49ea748 100644 --- a/plugins/scaffolder/src/components/index.ts +++ b/plugins/scaffolder/src/components/index.ts @@ -19,3 +19,4 @@ export { TemplateTypePicker } from './TemplateTypePicker'; export * from './secrets'; export { TaskPage } from './TaskPage'; export type { RouterProps } from './Router'; +export type { ReviewStepProps } from './types'; diff --git a/plugins/scaffolder/src/components/types.ts b/plugins/scaffolder/src/components/types.ts new file mode 100644 index 0000000000..f5989a2ae1 --- /dev/null +++ b/plugins/scaffolder/src/components/types.ts @@ -0,0 +1,37 @@ +/* + * 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 { UiSchema } from '@rjsf/utils'; +import { JsonObject } from '@backstage/types'; + +/** + * The props for the Last Step in scaffolder template form. + * Which represents the summary of the input provided by the end user. + * + * @public + */ +export type ReviewStepProps = { + disableButtons: boolean; + formData: JsonObject; + handleBack: () => void; + handleReset: () => void; + handleCreate: () => void; + steps: { + uiSchema: UiSchema; + mergedSchema: JsonObject; + schema: JsonObject; + }[]; +}; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index 8081dcb0ca..c6ab5276d0 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -13,40 +13,64 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EntityPicker } from '../components/fields/EntityPicker/EntityPicker'; -import { EntityNamePicker } from '../components/fields/EntityNamePicker/EntityNamePicker'; +import { + EntityPicker, + EntityPickerSchema, +} from '../components/fields/EntityPicker/EntityPicker'; +import { + EntityNamePicker, + EntityNamePickerSchema, +} from '../components/fields/EntityNamePicker/EntityNamePicker'; import { entityNamePickerValidation } from '../components/fields/EntityNamePicker/validation'; -import { EntityTagsPicker } from '../components/fields/EntityTagsPicker/EntityTagsPicker'; -import { OwnerPicker } from '../components/fields/OwnerPicker/OwnerPicker'; -import { RepoUrlPicker } from '../components/fields/RepoUrlPicker/RepoUrlPicker'; +import { + EntityTagsPicker, + EntityTagsPickerSchema, +} from '../components/fields/EntityTagsPicker/EntityTagsPicker'; +import { + OwnerPicker, + OwnerPickerSchema, +} from '../components/fields/OwnerPicker/OwnerPicker'; +import { + RepoUrlPicker, + RepoUrlPickerSchema, +} from '../components/fields/RepoUrlPicker/RepoUrlPicker'; import { repoPickerValidation } from '../components/fields/RepoUrlPicker/validation'; -import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { + OwnedEntityPicker, + OwnedEntityPickerSchema, +} from '../components/fields/OwnedEntityPicker/OwnedEntityPicker'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS = [ { component: EntityPicker, name: 'EntityPicker', + schema: EntityPickerSchema, }, { component: EntityNamePicker, name: 'EntityNamePicker', validation: entityNamePickerValidation, + schema: EntityNamePickerSchema, }, { component: EntityTagsPicker, name: 'EntityTagsPicker', + schema: EntityTagsPickerSchema, }, { component: RepoUrlPicker, name: 'RepoUrlPicker', validation: repoPickerValidation, + schema: RepoUrlPickerSchema, }, { component: OwnerPicker, name: 'OwnerPicker', + schema: OwnerPickerSchema, }, { component: OwnedEntityPicker, name: 'OwnedEntityPicker', + schema: OwnedEntityPickerSchema, }, ]; diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index 08075b5b9d..a3a4fdf57b 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { + CustomFieldExtensionSchema, CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, @@ -72,7 +73,7 @@ export function createNextScaffolderFieldExtension< TInputProps extends UIOptionsType = {}, >( options: NextFieldExtensionOptions, -): Extension> { +): Extension> { return { expose() { const FieldExtensionDataHolder: any = () => null; @@ -103,6 +104,7 @@ attachComponentData( ); export type { + CustomFieldExtensionSchema, CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, diff --git a/plugins/scaffolder/src/extensions/types.ts b/plugins/scaffolder/src/extensions/types.ts index 62f17dff15..3834b39c0a 100644 --- a/plugins/scaffolder/src/extensions/types.ts +++ b/plugins/scaffolder/src/extensions/types.ts @@ -15,14 +15,14 @@ */ import { ApiHolder } from '@backstage/core-plugin-api'; import { FieldValidation, FieldProps } from '@rjsf/core'; -import { PropsWithChildren } from 'react'; - import { UIOptionsType, FieldProps as FieldPropsV5, UiSchema as UiSchemaV5, FieldValidation as FieldValidationV5, } from '@rjsf/utils'; +import { PropsWithChildren } from 'react'; +import { JSONSchema7 } from 'json-schema'; /** * Field validation type for Custom Field Extensions. @@ -35,6 +35,16 @@ export type CustomFieldValidator = ( context: { apiHolder: ApiHolder }, ) => void | Promise; +/** + * Type for the Custom Field Extension schema. + * + * @public + */ +export type CustomFieldExtensionSchema = { + returnValue: JSONSchema7; + uiOptions?: JSONSchema7; +}; + /** * Type for the Custom Field Extension with the * name and components and validation function. @@ -50,6 +60,7 @@ export type FieldExtensionOptions< props: FieldExtensionComponentProps, ) => JSX.Element | null; validation?: CustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; /** @@ -107,4 +118,5 @@ export type NextFieldExtensionOptions< props: NextFieldExtensionComponentProps, ) => JSX.Element | null; validation?: NextCustomFieldValidator; + schema?: CustomFieldExtensionSchema; }; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 7853667101..0e5e2630ac 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -43,6 +43,7 @@ export { ScaffolderFieldExtensions, } from './extensions'; export type { + CustomFieldExtensionSchema, CustomFieldValidator, FieldExtensionOptions, FieldExtensionComponentProps, diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index ebe00fdfb8..0fc1c9eceb 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -30,6 +30,7 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups'; import { nextSelectedTemplateRouteRef } from '../../routes'; import { SecretsContextProvider } from '../../components/secrets/SecretsContext'; +import type { ErrorTransformer } from '@rjsf/utils'; /** * The Props for the Scaffolder Router @@ -44,6 +45,7 @@ export type NextRouterProps = { TaskPageComponent?: React.ComponentType<{}>; }; groups?: TemplateGroupFilter[]; + transformErrors?: ErrorTransformer; }; /** diff --git a/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx index e6022d16fb..3c2807c4c8 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/RegisterExistingButton.test.tsx @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { screen } from '@testing-library/react'; import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { RegisterExistingButton } from './RegisterExistingButton'; @@ -30,28 +32,22 @@ describe('RegisterExistingButton', () => { it('should not render if to is unset', async () => { (usePermission as jest.Mock).mockReturnValue({ allowed: true }); - const { queryByText } = await renderInTestApp( - , - ); + await renderInTestApp(); - expect(await queryByText('Pick me')).not.toBeInTheDocument(); + expect(screen.queryByText('Pick me')).not.toBeInTheDocument(); }); it('should not render if permissions are not allowed', async () => { (usePermission as jest.Mock).mockReturnValue({ allowed: false }); - const { queryByText } = await renderInTestApp( - , - ); + await renderInTestApp(); - expect(await queryByText('Pick me')).not.toBeInTheDocument(); + expect(screen.queryByText('Pick me')).not.toBeInTheDocument(); }); it('should render the button with the text', async () => { (usePermission as jest.Mock).mockReturnValue({ allowed: true }); - const { queryByText } = await renderInTestApp( - , - ); + await renderInTestApp(); - expect(await queryByText('Pick me')).toBeInTheDocument(); + expect(screen.getByText('Pick me')).toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx index 2b2e706102..c03df8f73c 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { ReviewState } from './ReviewState'; import { render } from '@testing-library/react'; @@ -97,9 +98,7 @@ describe('ReviewState', () => { , ); - expect( - await queryByRole('row', { name: 'Name ******' }), - ).not.toBeInTheDocument(); + expect(queryByRole('row', { name: 'Name ******' })).not.toBeInTheDocument(); }); it('should allow for masking an option with a set text', () => { diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx index 12ad05ab75..6c02492cd8 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx @@ -18,6 +18,8 @@ import { TemplateParameterSchema } from '../../../types'; import { Stepper } from './Stepper'; import { renderInTestApp } from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; +import type { RJSFValidationError } from '@rjsf/utils'; +import { JsonValue } from '@backstage/types'; describe('Stepper', () => { it('should render the step titles for each step of the manifest', async () => { @@ -140,4 +142,89 @@ describe('Stepper', () => { expect(getByText('im a custom field extension')).toBeInTheDocument(); }); + + it('should transform default error message', async () => { + const manifest: TemplateParameterSchema = { + steps: [ + { + title: 'Step 1', + schema: { + properties: { + postcode: { + type: 'string', + pattern: '[A-Z][0-9][A-Z] [0-9][A-Z][0-9]', + }, + }, + }, + }, + ], + title: 'transformErrors Form Test', + }; + + const transformErrors = (errors: RJSFValidationError[]) => { + return errors.map(err => + err.property === '.postcode' + ? { ...err, message: 'invalid postcode' } + : err, + ); + }; + + const { getByText, getByRole } = await renderInTestApp( + , + ); + + await fireEvent.change(getByRole('textbox', { name: 'postcode' }), { + target: { value: 'invalid' }, + }); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Review' })); + }); + + expect(getByText('invalid postcode')).toBeInTheDocument(); + }); + + it('should initialize formState with undefined form values', async () => { + const manifest: TemplateParameterSchema = { + steps: [ + { + title: 'Step 1', + schema: { + properties: { + firstName: { + type: 'string', + }, + }, + }, + }, + ], + title: 'initialize formData', + }; + + const onComplete = jest.fn(async (values: Record) => { + expect(values).toHaveProperty('firstName'); + }); + + const { getByRole } = await renderInTestApp( + , + ); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Review' })); + }); + + expect(getByRole('button', { name: 'Create' })).toBeInTheDocument(); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Create' })); + }); + + // flush promises + return new Promise(process.nextTick); + }); }); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index 511b020c3c..7051b197f1 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -13,8 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useApiHolder } from '@backstage/core-plugin-api'; -import { JsonObject, JsonValue } from '@backstage/types'; +import { + useAnalytics, + useApiHolder, + useRouteRefParams, +} from '@backstage/core-plugin-api'; +import { JsonValue } from '@backstage/types'; import { Stepper as MuiStepper, Step as MuiStep, @@ -31,6 +35,9 @@ import { createAsyncValidators } from './createAsyncValidators'; import { useTemplateSchema } from './useTemplateSchema'; import { ReviewState } from './ReviewState'; import validator from '@rjsf/validator-ajv8'; +import { selectedTemplateRouteRef } from '../../../routes'; +import type { ErrorTransformer } from '@rjsf/utils'; +import { getDefaultFormState } from '@rjsf/utils'; const useStyles = makeStyles(theme => ({ backButton: { @@ -51,6 +58,7 @@ export interface StepperProps { manifest: TemplateParameterSchema; extensions: NextFieldExtensionOptions[]; onComplete: (values: Record) => Promise; + transformErrors?: ErrorTransformer; } // TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly @@ -59,10 +67,12 @@ export interface StepperProps { const Form = withTheme(require('@rjsf/material-ui-v5').Theme); export const Stepper = (props: StepperProps) => { + const { templateName } = useRouteRefParams(selectedTemplateRouteRef); + const analytics = useAnalytics(); const { steps } = useTemplateSchema(props.manifest); const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); - const [formState, setFormState] = useState({}); + const [formState, setFormState] = useState>({}); const [errors, setErrors] = useState< undefined | Record >(); @@ -90,12 +100,27 @@ export const Stepper = (props: StepperProps) => { setActiveStep(prevActiveStep => prevActiveStep - 1); }; - const handleNext = async ({ formData }: { formData: JsonObject }) => { + const handleNext = async ({ + formData, + }: { + formData: Record; + }) => { // TODO(blam): What do we do about loading states, does each field extension get a chance // to display it's own loading? Or should we grey out the entire form. setErrors(undefined); - const returnedValidation = await validation(formData); + const schema = steps[activeStep]?.schema; + const rootSchema = steps[activeStep]?.mergedSchema; + + const newFormData = getDefaultFormState( + validator, + schema, + formData, + rootSchema, + true, + ); + + const returnedValidation = await validation(newFormData); const hasErrors = Object.values(returnedValidation).some( i => i.__errors?.length, @@ -105,9 +130,13 @@ export const Stepper = (props: StepperProps) => { setErrors(returnedValidation); } else { setErrors(undefined); - setActiveStep(prevActiveStep => prevActiveStep + 1); + setActiveStep(prevActiveStep => { + const stepNum = prevActiveStep + 1; + analytics.captureEvent('click', `Next Step (${stepNum})`); + return stepNum; + }); } - setFormState(current => ({ ...current, ...formData })); + setFormState(current => ({ ...current, ...newFormData })); }; return ( @@ -128,11 +157,13 @@ export const Stepper = (props: StepperProps) => { validator={validator} extraErrors={errors as unknown as ErrorSchema} formData={formState} + formContext={{ formData: formState }} schema={steps[activeStep].schema} uiSchema={steps[activeStep].uiSchema} onSubmit={handleNext} fields={extensions} showErrorList={false} + transformErrors={props.transformErrors} >
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx new file mode 100644 index 0000000000..f36fae9aa3 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -0,0 +1,115 @@ +/* + * 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 { ApiProvider } from '@backstage/core-app-api'; +import { analyticsApiRef } from '@backstage/core-plugin-api'; +import { + MockAnalyticsApi, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; +import { act, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { scaffolderApiRef } from '../../api'; +import { nextRouteRef, rootRouteRef } from '../../routes'; +import { ScaffolderApi } from '../../types'; +import { TemplateWizardPage } from './TemplateWizardPage'; + +jest.mock('react-router-dom', () => { + return { + ...(jest.requireActual('react-router-dom') as any), + useParams: () => ({ + templateName: 'test', + }), + }; +}); + +const scaffolderApiMock: jest.Mocked = { + scaffold: jest.fn(), + getTemplateParameterSchema: jest.fn(), + getIntegrationsList: jest.fn(), + getTask: jest.fn(), + streamLogs: jest.fn(), + listActions: jest.fn(), + listTasks: jest.fn(), +}; + +const analyticsMock = new MockAnalyticsApi(); +const apis = TestApiRegistry.from( + [scaffolderApiRef, scaffolderApiMock], + [analyticsApiRef, analyticsMock], +); + +describe('TemplateWizardPage', () => { + it('captures expected analytics events', async () => { + scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' }); + scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue({ + steps: [ + { + title: 'Step 1', + schema: { + properties: { + name: { + type: 'string', + }, + }, + }, + }, + ], + title: 'React JSON Schema Form Test', + }); + + const { findByRole, getByRole } = await renderInTestApp( + + , + , + { + mountedRoutes: { + '/create': nextRouteRef, + '/create-legacy': rootRouteRef, + }, + }, + ); + + // Fill out the name field + fireEvent.change(getByRole('textbox', { name: 'name' }), { + target: { value: 'expected-name' }, + }); + + // Go to the final page + await act(async () => { + fireEvent.click(await findByRole('button', { name: 'Review' })); + }); + + // Create the software + await act(async () => { + fireEvent.click(await findByRole('button', { name: 'Create' })); + }); + + // The "Next Step" button should have fired an event + expect(analyticsMock.getEvents()[0]).toMatchObject({ + action: 'click', + subject: 'Next Step (1)', + context: { entityRef: 'template:default/test' }, + }); + + // And the "Create" button should have fired an event + expect(analyticsMock.getEvents()[1]).toMatchObject({ + action: 'create', + subject: 'expected-name', + context: { entityRef: 'template:default/test' }, + }); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx index b650e4a676..79ba9d33c9 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx @@ -26,6 +26,7 @@ import { NextFieldExtensionOptions } from '../../extensions'; import { Navigate, useNavigate } from 'react-router'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { + AnalyticsContext, errorApiRef, useApi, useRouteRef, @@ -43,9 +44,11 @@ import { } from '../../routes'; import { SecretsContext } from '../../components/secrets/SecretsContext'; import { JsonValue } from '@backstage/types'; +import type { ErrorTransformer } from '@rjsf/utils'; export interface TemplateWizardPageProps { customFieldExtensions: NextFieldExtensionOptions[]; + transformErrors?: ErrorTransformer; } const useStyles = makeStyles(() => ({ @@ -111,34 +114,37 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { } return ( - -
- - {loading && } - {manifest && ( - + +
+ + {loading && } + {manifest && ( + + } + noPadding + titleTypographyProps={{ component: 'h2' }} + > + - } - noPadding - titleTypographyProps={{ component: 'h2' }} - > - - - )} - - + + )} + + + ); }; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.tsx similarity index 81% rename from plugins/scaffolder/src/plugin.ts rename to plugins/scaffolder/src/plugin.tsx index f9b359d93a..cfa7af8128 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.tsx @@ -16,12 +16,24 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef, ScaffolderClient } from './api'; -import { EntityPicker } from './components/fields/EntityPicker/EntityPicker'; +import { + EntityPicker, + EntityPickerSchema, +} from './components/fields/EntityPicker/EntityPicker'; import { entityNamePickerValidation } from './components/fields/EntityNamePicker'; -import { EntityNamePicker } from './components/fields/EntityNamePicker/EntityNamePicker'; -import { OwnerPicker } from './components/fields/OwnerPicker/OwnerPicker'; +import { + EntityNamePicker, + EntityNamePickerSchema, +} from './components/fields/EntityNamePicker/EntityNamePicker'; +import { + OwnerPicker, + OwnerPickerSchema, +} from './components/fields/OwnerPicker/OwnerPicker'; import { repoPickerValidation } from './components/fields/RepoUrlPicker'; -import { RepoUrlPicker } from './components/fields/RepoUrlPicker/RepoUrlPicker'; +import { + RepoUrlPicker, + RepoUrlPickerSchema, +} from './components/fields/RepoUrlPicker/RepoUrlPicker'; import { createScaffolderFieldExtension } from './extensions'; import { nextRouteRef, @@ -37,8 +49,14 @@ import { fetchApiRef, identityApiRef, } from '@backstage/core-plugin-api'; -import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker/OwnedEntityPicker'; -import { EntityTagsPicker } from './components/fields/EntityTagsPicker/EntityTagsPicker'; +import { + OwnedEntityPicker, + OwnedEntityPickerSchema, +} from './components/fields/OwnedEntityPicker/OwnedEntityPicker'; +import { + EntityTagsPicker, + EntityTagsPickerSchema, +} from './components/fields/EntityTagsPicker/EntityTagsPicker'; /** * The main plugin export for the scaffolder. @@ -82,6 +100,7 @@ export const EntityPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: EntityPicker, name: 'EntityPicker', + schema: EntityPickerSchema, }), ); @@ -95,12 +114,13 @@ export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( component: EntityNamePicker, name: 'EntityNamePicker', validation: entityNamePickerValidation, + schema: EntityNamePickerSchema, }), ); /** * The field extension which provides the ability to select a RepositoryUrl. - * Currently this is an encoded URL that looks something like the following `github.com?repo=myRepoName&owner=backstage`. + * Currently, this is an encoded URL that looks something like the following `github.com?repo=myRepoName&owner=backstage`. * * @public */ @@ -109,6 +129,7 @@ export const RepoUrlPickerFieldExtension = scaffolderPlugin.provide( component: RepoUrlPicker, name: 'RepoUrlPicker', validation: repoPickerValidation, + schema: RepoUrlPickerSchema, }), ); @@ -121,6 +142,7 @@ export const OwnerPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: OwnerPicker, name: 'OwnerPicker', + schema: OwnerPickerSchema, }), ); @@ -146,6 +168,7 @@ export const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: OwnedEntityPicker, name: 'OwnedEntityPicker', + schema: OwnedEntityPickerSchema, }), ); @@ -157,6 +180,7 @@ export const EntityTagsPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: EntityTagsPicker, name: 'EntityTagsPicker', + schema: EntityTagsPickerSchema, }), ); diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index 30a3367bf4..200f866368 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.0.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.5-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 1.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.4 + - @backstage/config@1.0.4 + - @backstage/plugin-search-common@1.1.1 + +## 1.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.4-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 1.0.4-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index a45c3c43e5..4ff22640cc 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "1.0.4-next.0", + "version": "1.0.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts index 3a237664a8..602a158b98 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts @@ -68,6 +68,14 @@ const customIndexTemplate = { }, }; +const advanceTimersByNTimes = async (n = 1, time = 1000) => { + for (let i = 0; i < n; i++) { + await Promise.resolve(); + jest.advanceTimersByTime(time); + await Promise.resolve(); + } +}; + describe('ElasticSearchSearchEngine', () => { let testSearchEngine: ElasticSearchSearchEngine; let inspectableSearchEngine: ElasticSearchSearchEngineForTranslatorTests; @@ -855,35 +863,33 @@ describe('ElasticSearchSearchEngine', () => { }); it('should check for and delete expected index', async () => { - const existsSpy = jest.fn().mockReturnValue('truthy value'); const deleteSpy = jest.fn().mockReturnValue({}); - mock.add({ method: 'HEAD', path: '/expected-index-name' }, existsSpy); mock.add({ method: 'DELETE', path: '/expected-index-name' }, deleteSpy); await errorHandler(error); // Check and delete HTTP requests were made. - expect(existsSpy).toHaveBeenCalled(); expect(deleteSpy).toHaveBeenCalled(); }); - it('should not delete index if none exists', async () => { - // Exists call returns 404 on no index. - const existsSpy = jest.fn().mockReturnValue( + it('should retry delete index up to 5 times', async () => { + // Delete call returns 404 + const deleteSpy = jest.fn().mockReturnValue( new errors.ResponseError({ statusCode: 404, body: { status: 404 }, } as unknown as any), ); - const deleteSpy = jest.fn().mockReturnValue({}); - mock.add({ method: 'HEAD', path: '/expected-index-name' }, existsSpy); mock.add({ method: 'DELETE', path: '/expected-index-name' }, deleteSpy); - await errorHandler(error); + // Call the error handler and advance timers + jest.useFakeTimers(); + errorHandler(error); + await advanceTimersByNTimes(10); + jest.useRealTimers(); - // Check request was made, but no delete request was made. - expect(existsSpy).toHaveBeenCalled(); - expect(deleteSpy).not.toHaveBeenCalled(); + // Check request was made 5 times + expect(deleteSpy).toHaveBeenCalledTimes(5); }); }); }); diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index a33d8939eb..f3db46956d 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -266,19 +266,41 @@ export class ElasticSearchSearchEngine implements SearchEngine { // Attempt cleanup upon failure. indexer.on('error', async e => { this.logger.error(`Failed to index documents for type ${type}`, e); - try { - const response = await this.elasticSearchClientWrapper.indexExists({ - index: indexer.indexName, - }); - const indexCreated = response.body; - if (indexCreated) { - this.logger.info(`Removing created index ${indexer.indexName}`); - await this.elasticSearchClientWrapper.deleteIndex({ - index: indexer.indexName, - }); + let cleanupError: Error | undefined; + + // In some cases, a failure may have occurred before the indexer was able + // to complete initialization. Try up to 5 times to remove the dangling + // index. + await new Promise(async done => { + const maxAttempts = 5; + let attempts = 0; + + while (attempts < maxAttempts) { + try { + await this.elasticSearchClientWrapper.deleteIndex({ + index: indexer.indexName, + }); + + attempts = maxAttempts; + cleanupError = undefined; + done(); + } catch (err) { + cleanupError = err; + } + + // Wait 1 second between retries. + await new Promise(okay => setTimeout(okay, 1000)); + + attempts++; } - } catch (error) { - this.logger.error(`Unable to clean up elastic index: ${error}`); + }); + + if (cleanupError) { + this.logger.error( + `Unable to clean up elastic index ${indexer.indexName}: ${cleanupError}`, + ); + } else { + this.logger.info(`Removed partial, failed index ${indexer.indexName}`); } }); @@ -335,6 +357,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { ), nextPageCursor, previousPageCursor, + numberOfResults: result.body.hits.total.value, }; } catch (error) { if (error.meta?.body?.error?.type === 'index_not_found_exception') { diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts index a19687a03b..155a6bbf32 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngineIndexer.test.ts @@ -137,7 +137,7 @@ describe('ElasticSearchSearchEngineIndexer', () => { }, ]; - await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); // Older indices should have been queried for. expect(catSpy).toHaveBeenCalled(); @@ -184,7 +184,7 @@ describe('ElasticSearchSearchEngineIndexer', () => { text: range(2000).join(', '), })); - await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); // Ensure multiple bulk requests were made. expect(bulkSpy).toHaveBeenCalledTimes(2); @@ -221,7 +221,7 @@ describe('ElasticSearchSearchEngineIndexer', () => { catSpy, ); - await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); // Final deletion shouldn't be called. expect(deleteSpy).not.toHaveBeenCalled(); diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index dcd0bbdc78..8a088380f0 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-search-backend-module-pg +## 0.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-search-backend-node@1.0.5-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.5-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 0.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-search-backend-node@1.0.4 + - @backstage/config@1.0.4 + - @backstage/plugin-search-common@1.1.1 + +## 0.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-search-backend-node@1.0.4-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 0.4.2-next.0 ### Patch Changes diff --git a/plugins/search-backend-module-pg/migrations/20210311110000_init.js b/plugins/search-backend-module-pg/migrations/20210311110000_init.js index 70ccf9406b..e3bbca41e3 100644 --- a/plugins/search-backend-module-pg/migrations/20210311110000_init.js +++ b/plugins/search-backend-module-pg/migrations/20210311110000_init.js @@ -14,6 +14,8 @@ * limitations under the License. */ +// @ts-check + /** * @param {import('knex').Knex} knex */ diff --git a/plugins/search-backend-module-pg/migrations/20210727180000_delta_update.js b/plugins/search-backend-module-pg/migrations/20210727180000_delta_update.js index 429c8aa805..2a449b5fa4 100644 --- a/plugins/search-backend-module-pg/migrations/20210727180000_delta_update.js +++ b/plugins/search-backend-module-pg/migrations/20210727180000_delta_update.js @@ -14,6 +14,8 @@ * limitations under the License. */ +// @ts-check + /** * @param {import('knex').Knex} knex */ diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 5e91bfabeb..25ee3cf696 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-pg", "description": "A module for the search backend that implements search using PostgreSQL", - "version": "0.4.2-next.0", + "version": "0.4.3-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts index 03b9774751..0b9cd96c19 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngineIndexer.test.ts @@ -53,7 +53,7 @@ describe('PgSearchEngineIndexer', () => { }, ]; - await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); expect(database.getTransaction).toHaveBeenCalledTimes(1); expect(database.prepareInsert).toHaveBeenCalledTimes(1); @@ -73,7 +73,7 @@ describe('PgSearchEngineIndexer', () => { location: `location-${i}`, })); - await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); expect(database.getTransaction).toHaveBeenCalledTimes(1); expect(database.prepareInsert).toHaveBeenCalledTimes(1); @@ -92,7 +92,7 @@ describe('PgSearchEngineIndexer', () => { ]; database.prepareInsert.mockRejectedValueOnce(expectedError); - const result = await TestPipeline.withSubject(indexer) + const result = await TestPipeline.fromIndexer(indexer) .withDocuments(documents) .execute(); @@ -114,7 +114,7 @@ describe('PgSearchEngineIndexer', () => { ]; database.insertDocuments.mockRejectedValueOnce(expectedError); - const result = await TestPipeline.withSubject(indexer) + const result = await TestPipeline.fromIndexer(indexer) .withDocuments(documents) .execute(); @@ -136,7 +136,7 @@ describe('PgSearchEngineIndexer', () => { ]; database.completeInsert.mockRejectedValueOnce(expectedError); - const result = await TestPipeline.withSubject(indexer) + const result = await TestPipeline.fromIndexer(indexer) .withDocuments(documents) .execute(); diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index bfcd09dcad..a1295f7130 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-search-backend-node +## 1.0.5-next.1 + +### Patch Changes + +- de8a975911: Changed to use native `AbortController` and `AbortSignal` from Node.js, instead + of the one from `node-abort-controller`. This is possible now that the minimum + supported Node.js version of the project is 16. + + Note that their interfaces are very slightly different, but typically not in a + way that matters to consumers. If you see any typescript errors as a direct + result from this, they are compatible with each other in the ways that we + interact with them, and should be possible to type-cast across without ill + effects. + +- 54c5836f7a: Use of `TestPipeline.withSubject()` is now deprecated. Instead, use the `fromCollator`, `fromDecorator`, or `fromIndexer` static methods to instantiate a test pipeline. You may also use the class' `withCollator`, `withDecorator`, and `withIndexer` instance methods to build test pipelines that consist of multiple test subjects. +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.0.5-next.0 + +### Patch Changes + +- a962ce0551: Wait for indexer initialization before finalizing indexing. +- 683ced83f6: Fixed a bug that could cause a `max listeners exceeded warning` to be logged when more than 10 collators were running simultaneously. +- 81b1e7b0fe: Updated indexer and decorator base classes to take advantage of features introduced in Node.js v16; be sure you are running a [supported version of Node.js](https://backstage.io/docs/releases/v1.8.0#node-16-and-18). +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 1.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-search-common@1.1.1 + +## 1.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 1.0.4-next.0 ### Patch Changes diff --git a/plugins/search-backend-node/api-report.md b/plugins/search-backend-node/api-report.md index 62a4eabc26..734bf6e1c0 100644 --- a/plugins/search-backend-node/api-report.md +++ b/plugins/search-backend-node/api-report.md @@ -173,7 +173,14 @@ export type ScheduleTaskParameters = { // @public export class TestPipeline { execute(): Promise; + static fromCollator(collator: Readable): TestPipeline; + static fromDecorator(decorator: Transform): TestPipeline; + static fromIndexer(indexer: Writable): TestPipeline; + withCollator(collator: Readable): this; + withDecorator(decorator: Transform): this; withDocuments(documents: IndexableDocument[]): TestPipeline; + withIndexer(indexer: Writable): this; + // @deprecated static withSubject(subject: Readable | Transform | Writable): TestPipeline; } diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index a0c4964ccf..19f1defbfc 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "1.0.4-next.0", + "version": "1.0.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,7 +33,6 @@ "lodash": "^4.17.21", "lunr": "^2.3.9", "ndjson": "^2.0.0", - "node-abort-controller": "^3.0.1", "uuid": "^8.3.2", "winston": "^3.2.1" }, diff --git a/plugins/search-backend-node/src/Scheduler.test.ts b/plugins/search-backend-node/src/Scheduler.test.ts index 30e5c7e416..6daae3013d 100644 --- a/plugins/search-backend-node/src/Scheduler.test.ts +++ b/plugins/search-backend-node/src/Scheduler.test.ts @@ -204,4 +204,40 @@ describe('Scheduler', () => { ); }); }); + + describe('stop', () => { + it('should abort tasks on stop', () => { + const run = jest.fn(); + + // Add tasks and interval to schedule + testScheduler.addToSchedule({ + id: '1', + task: jest.fn(), + scheduledRunner: { run }, + }); + testScheduler.addToSchedule({ + id: '2', + task: jest.fn(), + scheduledRunner: { run }, + }); + + // Starts scheduling process + testScheduler.start(); + + const signals = run.mock.calls.map(([options]) => options.signal); + + expect(signals).toHaveLength(2); + + for (const signal of signals) { + expect(signal.aborted).toBeFalsy(); + } + + // Stops scheduling process + testScheduler.stop(); + + for (const signal of signals) { + expect(signal.aborted).toBeTruthy(); + } + }); + }); }); diff --git a/plugins/search-backend-node/src/Scheduler.ts b/plugins/search-backend-node/src/Scheduler.ts index 872a410808..39c423263c 100644 --- a/plugins/search-backend-node/src/Scheduler.ts +++ b/plugins/search-backend-node/src/Scheduler.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { AbortController } from 'node-abort-controller'; import { Logger } from 'winston'; import { TaskFunction, TaskRunner } from '@backstage/backend-tasks'; @@ -40,13 +39,13 @@ export type ScheduleTaskParameters = { export class Scheduler { private logger: Logger; private schedule: { [id: string]: TaskEnvelope }; - private abortController: AbortController; + private abortControllers: AbortController[]; private isRunning: boolean; constructor(options: { logger: Logger }) { this.logger = options.logger; this.schedule = {}; - this.abortController = new AbortController(); + this.abortControllers = []; this.isRunning = false; } @@ -78,11 +77,13 @@ export class Scheduler { this.logger.info('Starting all scheduled search tasks.'); this.isRunning = true; Object.keys(this.schedule).forEach(id => { + const abortController = new AbortController(); + this.abortControllers.push(abortController); const { task, scheduledRunner } = this.schedule[id]; scheduledRunner.run({ id, fn: task, - signal: this.abortController.signal, + signal: abortController.signal, }); }); } @@ -92,7 +93,10 @@ export class Scheduler { */ stop() { this.logger.info('Stopping all scheduled search tasks.'); - this.abortController.abort(); + for (const abortController of this.abortControllers) { + abortController.abort(); + } + this.abortControllers = []; this.isRunning = false; } } diff --git a/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts index 7120fc7ce5..c08db8f28e 100644 --- a/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts +++ b/plugins/search-backend-node/src/collators/NewlineDelimitedJsonCollatorFactory.test.ts @@ -142,7 +142,7 @@ describe('DefaultCatalogCollatorFactory', () => { }); const collator = await factory.getCollator(); - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); expect(documents).toHaveLength(2); diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts index f0518cd71d..4ef380943f 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.test.ts @@ -381,7 +381,7 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -412,7 +412,7 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -452,7 +452,7 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -495,7 +495,7 @@ describe('LunrSearchEngine', () => { inspectableSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -544,7 +544,7 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -583,7 +583,7 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -623,7 +623,7 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -663,7 +663,7 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -708,7 +708,7 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -763,10 +763,10 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index-2', ); - await TestPipeline.withSubject(indexer1) + await TestPipeline.fromIndexer(indexer1) .withDocuments(mockDocuments) .execute(); - await TestPipeline.withSubject(indexer2) + await TestPipeline.fromIndexer(indexer2) .withDocuments(mockDocuments2) .execute(); @@ -811,7 +811,7 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -871,14 +871,14 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); const indexer2 = await getActualIndexer( testLunrSearchEngine, 'test-index-2', ); - await TestPipeline.withSubject(indexer2) + await TestPipeline.fromIndexer(indexer2) .withDocuments(mockDocuments2) .execute(); @@ -924,7 +924,7 @@ describe('LunrSearchEngine', () => { testLunrSearchEngine, 'test-index', ); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -974,7 +974,7 @@ describe('LunrSearchEngine', () => { })); const indexer = await getActualIndexer(testLunrSearchEngine, 'test-index'); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -1112,7 +1112,7 @@ describe('stopword testing', () => { const indexer = await getActualIndexer(testLunrSearchEngine, 'test-index'); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -1149,7 +1149,7 @@ describe('stopword testing', () => { const indexer = await getActualIndexer(testLunrSearchEngine, 'test-index'); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); @@ -1186,7 +1186,7 @@ describe('stopword testing', () => { const indexer = await getActualIndexer(testLunrSearchEngine, 'test-index'); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments(mockDocuments) .execute(); diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index d60f9951f4..ff2c828402 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -238,6 +238,7 @@ export class LunrSearchEngine implements SearchEngine { }), }, })), + numberOfResults: results.length, nextPageCursor, previousPageCursor, }; diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts index fbb6b153d0..ff76f0fa7c 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngineIndexer.test.ts @@ -58,7 +58,7 @@ describe('LunrSearchEngineIndexer', () => { }, ]; - await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); expect(lunrBuilderAddSpy).toHaveBeenCalledWith(documents[0]); }); @@ -70,7 +70,7 @@ describe('LunrSearchEngineIndexer', () => { location: `location-${i}`, })); - await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); expect(lunrBuilderAddSpy).toHaveBeenCalledTimes(350); }); @@ -84,7 +84,7 @@ describe('LunrSearchEngineIndexer', () => { }, ]; - await TestPipeline.withSubject(indexer).withDocuments(documents).execute(); + await TestPipeline.fromIndexer(indexer).withDocuments(documents).execute(); // Builder ref should be set to location (and only once). expect(lunrBuilderRefSpy).toHaveBeenCalledTimes(1); diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts index b4c9ff9e46..a51a479e7e 100644 --- a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.test.ts @@ -46,7 +46,7 @@ describe('BatchSearchEngineIndexer', () => { it('should work end-to-end', async () => { const indexer = new ConcreteBatchIndexer({ batchSize: 1 }); - await TestPipeline.withSubject(indexer) + await TestPipeline.fromIndexer(indexer) .withDocuments([document, document, document]) .execute(); expect(indexSpy).toHaveBeenCalledTimes(3); diff --git a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts index 24b90c2a59..b41dd524a9 100644 --- a/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts +++ b/plugins/search-backend-node/src/indexing/BatchSearchEngineIndexer.ts @@ -34,26 +34,10 @@ export type BatchSearchEngineOptions = { export abstract class BatchSearchEngineIndexer extends Writable { private batchSize: number; private currentBatch: IndexableDocument[] = []; - private initialized: Promise; constructor(options: BatchSearchEngineOptions) { super({ objectMode: true }); this.batchSize = options.batchSize; - - // @todo Once node v15 is minimum, convert to _construct implementation. - this.initialized = new Promise(done => { - // Necessary to allow concrete implementation classes to construct - // themselves before calling their initialize() methods. - setImmediate(async () => { - try { - await this.initialize(); - done(undefined); - } catch (e) { - assertError(e); - done(e); - } - }); - }); } /** @@ -73,6 +57,20 @@ export abstract class BatchSearchEngineIndexer extends Writable { */ public abstract finalize(): Promise; + /** + * Encapsulates initialization logic. + * @internal + */ + async _construct(done: (error?: Error | null | undefined) => void) { + try { + await this.initialize(); + done(); + } catch (e) { + assertError(e); + done(e); + } + } + /** * Encapsulates batch stream write logic. * @internal @@ -82,13 +80,6 @@ export abstract class BatchSearchEngineIndexer extends Writable { _e: any, done: (error?: Error | null) => void, ) { - // Wait for init before proceeding. Throw error if initialization failed. - const maybeError = await this.initialized; - if (maybeError) { - done(maybeError); - return; - } - this.currentBatch.push(doc); if (this.currentBatch.length < this.batchSize) { done(); diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts index 12c61bd7d3..875ef11940 100644 --- a/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.test.ts @@ -54,7 +54,7 @@ describe('DecoratorBase', () => { })); const decorator = new ConcreteDecorator(); - const { documents } = await TestPipeline.withSubject(decorator) + const { documents } = await TestPipeline.fromDecorator(decorator) .withDocuments([document, document, document]) .execute(); @@ -68,7 +68,7 @@ describe('DecoratorBase', () => { decorateSpy.mockResolvedValue(undefined); const decorator = new ConcreteDecorator(); - const { documents } = await TestPipeline.withSubject(decorator) + const { documents } = await TestPipeline.fromDecorator(decorator) .withDocuments([document, document, document]) .execute(); @@ -82,7 +82,7 @@ describe('DecoratorBase', () => { }); const decorator = new ConcreteDecorator(); - const { documents } = await TestPipeline.withSubject(decorator) + const { documents } = await TestPipeline.fromDecorator(decorator) .withDocuments([document, document, document]) .execute(); diff --git a/plugins/search-backend-node/src/indexing/DecoratorBase.ts b/plugins/search-backend-node/src/indexing/DecoratorBase.ts index bcf5729674..9ed8dfe0a7 100644 --- a/plugins/search-backend-node/src/indexing/DecoratorBase.ts +++ b/plugins/search-backend-node/src/indexing/DecoratorBase.ts @@ -24,25 +24,8 @@ import { Transform } from 'stream'; * @public */ export abstract class DecoratorBase extends Transform { - private initialized: Promise; - constructor() { super({ objectMode: true }); - - // @todo Once node v15 is minimum, convert to _construct implementation. - this.initialized = new Promise(done => { - // Necessary to allow concrete implementation classes to construct - // themselves before calling their initialize() methods. - setImmediate(async () => { - try { - await this.initialize(); - done(undefined); - } catch (e) { - assertError(e); - done(e); - } - }); - }); } /** @@ -68,6 +51,20 @@ export abstract class DecoratorBase extends Transform { */ public abstract finalize(): Promise; + /** + * Encapsulates initialization logic. + * @internal + */ + async _construct(done: (error?: Error | null | undefined) => void) { + try { + await this.initialize(); + done(); + } catch (e) { + assertError(e); + done(e); + } + } + /** * Encapsulates simple transform stream logic. * @internal @@ -77,13 +74,6 @@ export abstract class DecoratorBase extends Transform { _: any, done: (error?: Error | null) => void, ) { - // Wait for init before proceeding. Throw error if initialization failed. - const maybeError = await this.initialized; - if (maybeError) { - done(maybeError); - return; - } - try { const decorated = await this.decorate(document); diff --git a/plugins/search-backend-node/src/test-utils/TestPipeline.ts b/plugins/search-backend-node/src/test-utils/TestPipeline.ts index 00dd9b3755..33097daae9 100644 --- a/plugins/search-backend-node/src/test-utils/TestPipeline.ts +++ b/plugins/search-backend-node/src/test-utils/TestPipeline.ts @@ -37,6 +37,35 @@ export type TestPipelineResult = { /** * Test utility for Backstage Search collators, decorators, and indexers. + * + * @example + * An example test checking that a collator provides expected documents. + * ``` + * it('provides expected documents', async () => { + * const testSubject = await yourCollatorFactory.getCollator(); + * const pipeline = TestPipeline.fromCollator(testSubject); + * + * const { documents } = await pipeline.execute(); + * + * expect(documents).toHaveLength(2); + * }) + * ``` + * + * @example + * An example test checking that a decorator behaves as expected. + * ``` + * it('filters private documents', async () => { + * const testSubject = await yourDecoratorFactory.getDecorator(); + * const pipeline = TestPipeline + * .fromDecorator(testSubject) + * .withDocuments([{ title: 'Private', location: '/private', text: '' }]); + * + * const { documents } = await pipeline.execute(); + * + * expect(documents).toHaveLength(0); + * }) + * ``` + * * @public */ export class TestPipeline { @@ -60,6 +89,9 @@ export class TestPipeline { /** * Provide the collator, decorator, or indexer to be tested. + * + * @deprecated Use `fromCollator`, `fromDecorator` or `fromIndexer` static + * methods to create a test pipeline instead. */ static withSubject(subject: Readable | Transform | Writable) { if (subject instanceof Transform) { @@ -79,6 +111,51 @@ export class TestPipeline { ); } + /** + * Create a test pipeline given a collator you want to test. + */ + static fromCollator(collator: Readable) { + return new TestPipeline({ collator }); + } + + /** + * Add a collator to the test pipeline. + */ + withCollator(collator: Readable): this { + this.collator = collator; + return this; + } + + /** + * Create a test pipeline given a decorator you want to test. + */ + static fromDecorator(decorator: Transform) { + return new TestPipeline({ decorator }); + } + + /** + * Add a decorator to the test pipeline. + */ + withDecorator(decorator: Transform): this { + this.decorator = decorator; + return this; + } + + /** + * Create a test pipeline given an indexer you want to test. + */ + static fromIndexer(indexer: Writable) { + return new TestPipeline({ indexer }); + } + + /** + * Add an indexer to the test pipeline. + */ + withIndexer(indexer: Writable): this { + this.indexer = indexer; + return this; + } + /** * Provide documents for testing decorators and indexers. */ diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index c2cc89ac5b..ff9bc68f38 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-search-backend +## 1.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-search-backend-node@1.0.5-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/plugin-permission-node@0.7.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-node@1.0.5-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/plugin-permission-node@0.7.2-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 1.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/types@1.0.1 + - @backstage/plugin-permission-node@0.7.1 + - @backstage/plugin-search-backend-node@1.0.4 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-search-common@1.1.1 + +## 1.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/plugin-permission-node@0.7.1-next.1 + - @backstage/plugin-search-backend-node@1.0.4-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 1.1.1-next.0 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index d221cf1ea5..b4479f86bc 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "1.1.1-next.0", + "version": "1.1.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts index ade43280be..52c87d76c7 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts @@ -205,6 +205,7 @@ export class AuthorizedSearchEngine implements SearchEngine { (nextPageCursor || filteredResults.length > targetResults) ? encodePageCursor({ page: page + 1 }) : undefined, + numberOfResults: undefined, }; } diff --git a/plugins/search-common/CHANGELOG.md b/plugins/search-common/CHANGELOG.md index 4ed96d5397..1ac65fda99 100644 --- a/plugins/search-common/CHANGELOG.md +++ b/plugins/search-common/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-search-common +## 1.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + +## 1.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/types@1.0.2-next.0 + +## 1.1.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.7.1 + - @backstage/types@1.0.1 + ## 1.1.1-next.0 ### Patch Changes diff --git a/plugins/search-common/api-report.md b/plugins/search-common/api-report.md index 61e5d22257..2006689ab9 100644 --- a/plugins/search-common/api-report.md +++ b/plugins/search-common/api-report.md @@ -73,6 +73,8 @@ export interface ResultSet { // (undocumented) nextPageCursor?: string; // (undocumented) + numberOfResults?: number; + // (undocumented) previousPageCursor?: string; // (undocumented) results: Result[]; diff --git a/plugins/search-common/package.json b/plugins/search-common/package.json index a0ec2513b4..e99c8d3c20 100644 --- a/plugins/search-common/package.json +++ b/plugins/search-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-common", "description": "Common functionalities for Search, to be shared between various search-enabled plugins", - "version": "1.1.1-next.0", + "version": "1.1.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/plugins/search-common/src/types.ts b/plugins/search-common/src/types.ts index 54abc9e1b1..2ea4a490ca 100644 --- a/plugins/search-common/src/types.ts +++ b/plugins/search-common/src/types.ts @@ -87,6 +87,7 @@ export interface ResultSet { results: Result[]; nextPageCursor?: string; previousPageCursor?: string; + numberOfResults?: number; } /** diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index d5f100d97c..893590d327 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-search-react +## 1.2.2-next.1 + +### Patch Changes + +- a19cffbeed: Update search links to only have header as linkable text +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.2 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 1.2.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/version-bridge@1.0.2 + - @backstage/core-plugin-api@1.1.0 + - @backstage/types@1.0.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.1 + +## 1.2.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/version-bridge@1.0.1 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 1.2.1-next.0 ### Patch Changes diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 7097d8fa4e..f88dc55302 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.2.1-next.0", + "version": "1.2.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx index f4260525d6..830cb61780 100644 --- a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -66,21 +66,23 @@ export const DefaultResultListItemComponent = ({ }; return ( - + <> {icon && {icon}} - ) : ( - result.title - ) + + {highlight?.fields.title ? ( + + ) : ( + result.title + )} + } secondary={ {secondaryAction}} - + ); }; diff --git a/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx index f900e45d45..b19af6eef8 100644 --- a/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx +++ b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx @@ -17,20 +17,34 @@ import React, { useEffect } from 'react'; import { useAnalytics } from '@backstage/core-plugin-api'; import { useSearch } from '../../context'; +import usePrevious from 'react-use/lib/usePrevious'; /** * Capture search event on term change. */ export const TrackSearch = ({ children }: { children: React.ReactChild }) => { + const useHasChanged = (value: any) => { + const previousVal = usePrevious(value); + return previousVal !== value; + }; + const analytics = useAnalytics(); - const { term } = useSearch(); + const { term, result } = useSearch(); + + const numberOfResults = result.value?.numberOfResults ?? undefined; + + // Stops the analtyics event from firing before the new search engine response is returned + const hasStartedLoading = useHasChanged(result.loading); + const hasFinishedLoading = hasStartedLoading && !result.loading; useEffect(() => { - if (term) { - // Capture analytics search event with search term provided as value - analytics.captureEvent('search', term); + if (term && hasFinishedLoading) { + // Capture analytics search event with search term and numberOfResults (provided as value) + analytics.captureEvent('search', term, { + value: numberOfResults, + }); } - }, [analytics, term]); + }, [analytics, term, numberOfResults, hasFinishedLoading]); return <>{children}; }; diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 5e71299d1e..b3fae44279 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,74 @@ # @backstage/plugin-search +## 1.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.0.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.2 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## 1.0.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/version-bridge@1.0.2 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/plugin-search-react@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.1 + +## 1.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/version-bridge@1.0.1 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + ## 1.0.4-next.0 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 7d0f6d4f52..63c1e745fd 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "1.0.4-next.0", + "version": "1.0.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -66,7 +66,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 661950e6ca..f90e2e2b67 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-sentry +## 0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.4.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.4.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.4.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.4.4-next.0 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 190e9b048b..4e9ee0855c 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.4.4-next.0", + "version": "0.4.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -62,7 +62,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index e918aa8608..761e108300 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-shortcuts +## 0.3.4-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/theme@0.2.16 + +## 0.3.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/theme@0.2.16 + +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/types@1.0.1 + - @backstage/theme@0.2.16 + +## 0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + ## 0.3.3-next.0 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 2bd0204af7..9801d321ba 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.3.3-next.0", + "version": "0.3.4-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,7 +34,7 @@ "react-hook-form": "^7.12.2", "react-use": "^17.2.4", "uuid": "^8.3.2", - "zen-observable": "^0.8.15" + "zen-observable": "^0.10.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", @@ -50,7 +50,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/sonarqube-backend/CHANGELOG.md b/plugins/sonarqube-backend/CHANGELOG.md index e752ee9a2b..b32dbe6430 100644 --- a/plugins/sonarqube-backend/CHANGELOG.md +++ b/plugins/sonarqube-backend/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-sonarqube-backend +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + ## 0.1.3-next.0 ### Patch Changes diff --git a/plugins/sonarqube-backend/package.json b/plugins/sonarqube-backend/package.json index 59f02b2950..5c1191d754 100644 --- a/plugins/sonarqube-backend/package.json +++ b/plugins/sonarqube-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube-backend", - "version": "0.1.3-next.0", + "version": "0.1.4-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,7 +36,7 @@ "@backstage/cli": "workspace:^", "@backstage/test-utils": "workspace:^", "@types/supertest": "^2.0.12", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.2.4" }, "files": [ diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index dd5aa80817..48f42d6bb2 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/plugin-sonarqube +## 0.5.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.5.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 17a8e32f39: Updated dependency `rc-progress` to `3.4.1`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.5.0 + +### Minor Changes + +- 786117e98a: Fix sonarqube annotation parsing. Add content page for Sonarqube. + Removed the deprecated `plugin` export; please use `sonarQubePlugin` instead. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.4.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.4.3-next.0 ### Patch Changes diff --git a/plugins/sonarqube/api-report.md b/plugins/sonarqube/api-report.md index e0a51479a4..2edf80618e 100644 --- a/plugins/sonarqube/api-report.md +++ b/plugins/sonarqube/api-report.md @@ -21,9 +21,17 @@ export const EntitySonarQubeCard: (props: { duplicationRatings?: DuplicationRating[] | undefined; }) => JSX.Element; +// @public (undocumented) +export const EntitySonarQubeContentPage: ( + props: SonarQubeContentPageProps, +) => JSX.Element; + // @public (undocumented) export const isSonarQubeAvailable: (entity: Entity) => boolean; +// @public (undocumented) +export const SONARQUBE_PROJECT_KEY_ANNOTATION = 'sonarqube.org/project-key'; + // @public (undocumented) export const SonarQubeCard: (props: { variant?: InfoCardVariants; @@ -31,7 +39,11 @@ export const SonarQubeCard: (props: { }) => JSX.Element; // @public (undocumented) -const sonarQubePlugin: BackstagePlugin<{}, {}, {}>; -export { sonarQubePlugin as plugin }; -export { sonarQubePlugin }; +export type SonarQubeContentPageProps = { + title?: string; + supportTitle?: string; +}; + +// @public (undocumented) +export const sonarQubePlugin: BackstagePlugin<{}, {}, {}>; ``` diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index b46cb5c0b1..3f0ac89e28 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sonarqube", "description": "", - "version": "0.4.3-next.0", + "version": "0.5.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -44,7 +44,7 @@ "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.10.0", "cross-fetch": "^3.1.5", - "rc-progress": "3.4.0", + "rc-progress": "3.4.1", "react-use": "^17.2.4" }, "peerDependencies": { @@ -59,7 +59,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.test.tsx b/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.test.tsx new file mode 100644 index 0000000000..8e847d7c31 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 { EntityProvider } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import React from 'react'; +import { + isSonarQubeAvailable, + SONARQUBE_PROJECT_KEY_ANNOTATION, +} from '../useProjectKey'; +import { SonarQubeApi, sonarQubeApiRef } from '../../api'; + +const Providers = ({ + annotation, + children, +}: { annotation: string } & React.PropsWithChildren): JSX.Element => ( + + + {children} + + +); + +describe('', () => { + beforeAll(() => { + jest.mock('@backstage/plugin-sonarqube', () => ({ + __esModule: true, + isSonarQubeAvailable, + EntitySonarQubeCard: () => { + return
; + }, + })); + }); + + it('renders EntitySonarQubeCard', async () => { + const { SonarQubeContentPage } = require('./SonarQubeContentPage'); + + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('SonarQube Dashboard')).toBeInTheDocument(); + expect(rendered.getByText('No information to display')).toBeInTheDocument(); + expect( + rendered.getByText("There is no SonarQube project with key 'bar'."), + ).toBeInTheDocument(); + }, 15000); + + it('renders MissingAnnotationEmptyState if sonar annotation is missing', async () => { + const { SonarQubeContentPage } = require('./SonarQubeContentPage'); + + const rendered = await renderInTestApp( + + + , + ); + expect(rendered.getByText('Missing Annotation')).toBeInTheDocument(); + expect( + rendered.getAllByText(SONARQUBE_PROJECT_KEY_ANNOTATION, { exact: false }) + .length, + ).toBe(2); + }, 15000); +}); diff --git a/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.tsx b/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.tsx new file mode 100644 index 0000000000..33a145b746 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeContentPage/SonarQubeContentPage.tsx @@ -0,0 +1,53 @@ +/* + * 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 { + Content, + ContentHeader, + SupportButton, +} from '@backstage/core-components'; +import { MissingAnnotationEmptyState } from '@backstage/core-components'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { + isSonarQubeAvailable, + SONARQUBE_PROJECT_KEY_ANNOTATION, +} from '../useProjectKey'; +import { SonarQubeCard } from '../SonarQubeCard'; + +/** @public */ +export type SonarQubeContentPageProps = { + title?: string; + supportTitle?: string; +}; + +export const SonarQubeContentPage = (props: SonarQubeContentPageProps) => { + const { entity } = useEntity(); + const { title, supportTitle } = props; + + return isSonarQubeAvailable(entity) ? ( + + + {supportTitle && {supportTitle}} + + + + ) : ( + + ); +}; diff --git a/plugins/sonarqube/src/components/SonarQubeContentPage/index.ts b/plugins/sonarqube/src/components/SonarQubeContentPage/index.ts new file mode 100644 index 0000000000..59327e9a88 --- /dev/null +++ b/plugins/sonarqube/src/components/SonarQubeContentPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { SonarQubeContentPage } from './SonarQubeContentPage'; +export type { SonarQubeContentPageProps } from './SonarQubeContentPage'; diff --git a/plugins/sonarqube/src/components/index.ts b/plugins/sonarqube/src/components/index.ts index 7e58100efa..8e1d93f85b 100644 --- a/plugins/sonarqube/src/components/index.ts +++ b/plugins/sonarqube/src/components/index.ts @@ -14,5 +14,10 @@ * limitations under the License. */ -export * from './SonarQubeCard'; -export { isSonarQubeAvailable } from './useProjectKey'; +export { SonarQubeCard } from './SonarQubeCard'; +export type { DuplicationRating } from './SonarQubeCard'; +export type { SonarQubeContentPageProps } from './SonarQubeContentPage'; +export { + isSonarQubeAvailable, + SONARQUBE_PROJECT_KEY_ANNOTATION, +} from './useProjectKey'; diff --git a/plugins/sonarqube/src/components/useProjectKey.test.ts b/plugins/sonarqube/src/components/useProjectKey.test.ts index 921e9455c0..8686ac82de 100644 --- a/plugins/sonarqube/src/components/useProjectKey.test.ts +++ b/plugins/sonarqube/src/components/useProjectKey.test.ts @@ -39,10 +39,12 @@ describe('isSonarQubeAvailable', () => { const entity = createDummyEntity('dummy'); expect(isSonarQubeAvailable(entity)).toBe(true); }); + it('returns false if sonarqube annotation empty', () => { const entity = createDummyEntity(''); expect(isSonarQubeAvailable(entity)).toBe(false); }); + it('returns false if sonarqube annotation not defined', () => { const entity = { apiVersion: '', @@ -59,6 +61,7 @@ describe('isSonarQubeAvailable', () => { describe('useProjectInfo', () => { const DUMMY_INSTANCE = 'dummyInstance'; const DUMMY_KEY = 'dummyKey'; + it('parse annotation with key and instance', () => { const entity = createDummyEntity( DUMMY_INSTANCE + SONARQUBE_PROJECT_INSTANCE_SEPARATOR + DUMMY_KEY, @@ -68,6 +71,33 @@ describe('useProjectInfo', () => { projectKey: DUMMY_KEY, }); }); + + it('parse annotation with instance, tenant/project-key', () => { + const DUMMY_KEY_WITH_TENANT = 'dummy-tenant/dummyKey'; + const entity = createDummyEntity( + DUMMY_INSTANCE + + SONARQUBE_PROJECT_INSTANCE_SEPARATOR + + DUMMY_KEY_WITH_TENANT, + ); + expect(useProjectInfo(entity)).toEqual({ + projectInstance: DUMMY_INSTANCE, + projectKey: DUMMY_KEY_WITH_TENANT, + }); + }); + + it('parse annotation with instance, tenant:project-key', () => { + const DUMMY_KEY_WITH_TENANT = 'dummy-tenant:dummyKey'; + const entity = createDummyEntity( + DUMMY_INSTANCE + + SONARQUBE_PROJECT_INSTANCE_SEPARATOR + + DUMMY_KEY_WITH_TENANT, + ); + expect(useProjectInfo(entity)).toEqual({ + projectInstance: DUMMY_INSTANCE, + projectKey: DUMMY_KEY_WITH_TENANT, + }); + }); + // compatibility with previous mono-instance sonarqube config it('parse annotation with only key', () => { const entity = createDummyEntity(DUMMY_KEY); @@ -76,6 +106,7 @@ describe('useProjectInfo', () => { projectKey: DUMMY_KEY, }); }); + it('handle empty annotation', () => { const entity = createDummyEntity(''); expect(useProjectInfo(entity)).toEqual({ @@ -83,6 +114,7 @@ describe('useProjectInfo', () => { projectKey: undefined, }); }); + it('handle non-existent annotation', () => { const entity = { apiVersion: '', diff --git a/plugins/sonarqube/src/components/useProjectKey.ts b/plugins/sonarqube/src/components/useProjectKey.ts index 59a572a6c4..20f7f03bb3 100644 --- a/plugins/sonarqube/src/components/useProjectKey.ts +++ b/plugins/sonarqube/src/components/useProjectKey.ts @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; +/** @public */ export const SONARQUBE_PROJECT_KEY_ANNOTATION = 'sonarqube.org/project-key'; export const SONARQUBE_PROJECT_INSTANCE_SEPARATOR = '/'; @@ -42,11 +43,16 @@ export const useProjectInfo = ( const annotation = entity?.metadata.annotations?.[SONARQUBE_PROJECT_KEY_ANNOTATION]; if (annotation) { - if (annotation.indexOf(SONARQUBE_PROJECT_INSTANCE_SEPARATOR) > -1) { - [projectInstance, projectKey] = annotation.split( - SONARQUBE_PROJECT_INSTANCE_SEPARATOR, - 2, - ); + const instanceSeparatorIndex = annotation.indexOf( + SONARQUBE_PROJECT_INSTANCE_SEPARATOR, + ); + if (instanceSeparatorIndex > -1) { + // Examples: + // instanceA/projectA -> projectInstance = "instanceA" & projectKey = "projectA" + // instanceA/tenantA:projectA -> projectInstance = "instanceA" & projectKey = "tenantA:projectA" + // instanceA/tenantA/projectA -> projectInstance = "instanceA" & projectKey = "tenantA/projectA" + projectInstance = annotation.substring(0, instanceSeparatorIndex); + projectKey = annotation.substring(instanceSeparatorIndex + 1); } else { projectKey = annotation; } diff --git a/plugins/sonarqube/src/index.ts b/plugins/sonarqube/src/index.ts index 1e6f83aa49..30f6351d4d 100644 --- a/plugins/sonarqube/src/index.ts +++ b/plugins/sonarqube/src/index.ts @@ -22,8 +22,4 @@ */ export * from './components'; -export { - sonarQubePlugin, - sonarQubePlugin as plugin, - EntitySonarQubeCard, -} from './plugin'; +export * from './plugin'; diff --git a/plugins/sonarqube/src/plugin.ts b/plugins/sonarqube/src/plugin.ts index 0ea62de492..5eb5ea82f3 100644 --- a/plugins/sonarqube/src/plugin.ts +++ b/plugins/sonarqube/src/plugin.ts @@ -52,3 +52,16 @@ export const EntitySonarQubeCard = sonarQubePlugin.provide( }, }), ); + +/** @public */ +export const EntitySonarQubeContentPage = sonarQubePlugin.provide( + createComponentExtension({ + name: 'EntitySonarQubeContentPage', + component: { + lazy: () => + import('./components/SonarQubeContentPage').then( + m => m.SonarQubeContentPage, + ), + }, + }), +); diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index 02a736b782..1e99d05403 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-splunk-on-call +## 0.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.4.1-next.0 + +### Patch Changes + +- cb716004ef: Internal refactor to improve tests +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.4.0 + +### Minor Changes + +- 34b772ef31: Use the routing key if it's available instead of team name when triggering incidents. + + BREAKING CHANGE: + Before, the team name was used even if the routing key (with or without team) was used. + Now, the routing key defined for the component will be used instead of the team name. + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/theme@0.2.16 + +## 0.4.0-next.1 + +### Minor Changes + +- 34b772ef31: Use the routing key if it's available instead of team name when triggering incidents. + + BREAKING CHANGE: + Before, the team name was used even if the routing key (with or without team) was used. + Now, the routing key defined for the component will be used instead of the team name. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.3.35-next.0 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 0434c7b07b..a47d691e25 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-splunk-on-call", "description": "A Backstage plugin that integrates towards Splunk On-Call", - "version": "0.3.35-next.0", + "version": "0.4.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -60,7 +60,7 @@ "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "configSchema": "config.d.ts", "files": [ diff --git a/plugins/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts index aa655a4772..2c5e122ac8 100644 --- a/plugins/splunk-on-call/src/api/client.ts +++ b/plugins/splunk-on-call/src/api/client.ts @@ -64,7 +64,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi { 'proxy', )}/splunk-on-call/v1/incidents`; - const { incidents } = await this.getByUrl(url); + const { incidents } = await this.findByUrl(url); return incidents; } @@ -73,7 +73,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi { const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/splunk-on-call/v1/oncall/current`; - const { teamsOnCall } = await this.getByUrl(url); + const { teamsOnCall } = await this.findByUrl(url); return teamsOnCall; } @@ -82,7 +82,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi { const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/splunk-on-call/v1/team`; - const teams = await this.getByUrl(url); + const teams = await this.findByUrl(url); return teams; } @@ -91,7 +91,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi { const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/splunk-on-call/v1/org/routing-keys`; - const { routingKeys } = await this.getByUrl(url); + const { routingKeys } = await this.findByUrl(url); return routingKeys; } @@ -100,7 +100,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi { const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/splunk-on-call/v2/user`; - const { users } = await this.getByUrl(url); + const { users } = await this.findByUrl(url); return users; } @@ -109,7 +109,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi { const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', )}/splunk-on-call/v1/policies`; - const { policies } = await this.getByUrl(url); + const { policies } = await this.findByUrl(url); return policies; } @@ -146,7 +146,7 @@ export class SplunkOnCallClient implements SplunkOnCallApi { return this.request(url, options); } - private async getByUrl(url: string): Promise { + private async findByUrl(url: string): Promise { const options = { method: 'GET', headers: { diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index 0688fb8237..704d2d9ed9 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -13,11 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { act, fireEvent, render, waitFor } from '@testing-library/react'; + import { Entity } from '@backstage/catalog-model'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; +import { + alertApiRef, + ConfigApi, + configApiRef, +} from '@backstage/core-plugin-api'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; +import { act, fireEvent, render, waitFor } from '@testing-library/react'; +import React from 'react'; import { splunkOnCallApiRef, SplunkOnCallClient, @@ -33,13 +40,7 @@ import { MOCK_TEAM_NO_INCIDENTS, } from '../api/mocks'; import { EntitySplunkOnCallCard } from './EntitySplunkOnCallCard'; - -import { - alertApiRef, - ConfigApi, - configApiRef, -} from '@backstage/core-plugin-api'; -import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; +import { expectTriggeredIncident } from './TriggerDialog/testUtils'; const mockSplunkOnCallApi: Partial = { getUsers: async () => [], @@ -167,8 +168,10 @@ describe('SplunkOnCallCard', () => { mockSplunkOnCallApi.getTeams = jest .fn() .mockImplementation(async () => [MOCK_TEAM]); + const mockTriggerAlarmFn = jest.fn(); + mockSplunkOnCallApi.incidentAction = mockTriggerAlarmFn; - const { getByText, queryByTestId } = render( + const { getByRole, getByTestId, getByText, queryByTestId } = render( wrapInTestApp( @@ -179,10 +182,23 @@ describe('SplunkOnCallCard', () => { ); await waitFor(() => !queryByTestId('progress')); expect(getByText(`Team: ${MOCK_TEAM.name}`)).toBeInTheDocument(); + expect(getByText('Create Incident')).toBeInTheDocument(); await waitFor( () => expect(getByText('test-incident')).toBeInTheDocument(), { timeout: 2000 }, ); + + const createIncidentButton = getByText('Create Incident'); + await act(async () => { + fireEvent.click(createIncidentButton); + }); + expect(getByRole('dialog')).toBeInTheDocument(); + + await expectTriggeredIncident( + 'test-routing-key', + getByTestId, + mockTriggerAlarmFn, + ); }); it('Handles custom error for missing token', async () => { diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index d635f7328f..26521ea9ee 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -29,11 +29,11 @@ import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; import WebIcon from '@material-ui/icons/Web'; import { Alert } from '@material-ui/lab'; import { splunkOnCallApiRef, UnauthorizedError } from '../api'; -import { MissingApiKeyOrApiIdError } from './Errors/MissingApiKeyOrApiIdError'; +import { MissingApiKeyOrApiIdError } from './Errors'; import { EscalationPolicy } from './Escalation'; import { Incidents } from './Incident'; import { TriggerDialog } from './TriggerDialog'; -import { Team, User } from './types'; +import { RoutingKey, Team, User } from './types'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { @@ -143,7 +143,7 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { }, []); const { - value: usersAndTeams, + value: entityData, loading, error, } = useAsync(async () => { @@ -162,11 +162,15 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { teams.find(teamValue => teamValue.name === teamAnnotation), ].filter(team => team !== undefined); - if (!foundTeams.length && routingKeyAnnotation) { + let foundRoutingKey: RoutingKey | undefined; + if (routingKeyAnnotation) { const routingKeys = await api.getRoutingKeys(); - const foundRoutingKey = routingKeys.find( + foundRoutingKey = routingKeys.find( key => key.routingKey === routingKeyAnnotation, ); + } + + if (!foundTeams.length) { foundTeams = foundRoutingKey ? foundRoutingKey.targets .map(target => { @@ -179,7 +183,7 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { : []; } - return { usersHashMap, foundTeams }; + return { usersHashMap, foundTeams, foundRoutingKey }; }); if (!teamAnnotation && !routingKeyAnnotation) { @@ -206,7 +210,7 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { return ; } - if (!usersAndTeams?.foundTeams || !usersAndTeams?.foundTeams.length) { + if (!entityData?.foundTeams || !entityData?.foundTeams.length) { return ( { const Content = ({ team, + routingKey, usersHashMap, }: { team: Team | undefined; + routingKey: RoutingKey | undefined; usersHashMap: any; }) => { const teamName = team?.name ?? ''; @@ -235,7 +241,7 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { )} { icon: , }; - const teams = usersAndTeams?.foundTeams || []; + const teams = entityData?.foundTeams || []; return ( <> @@ -277,7 +283,11 @@ export const EntitySplunkOnCallCard = (props: EntitySplunkOnCallCardProps) => { /> - + ))} diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx index 0b64a137af..3bfbddb0e4 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { render, fireEvent, act } from '@testing-library/react'; -import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; -import { splunkOnCallApiRef } from '../../api'; -import { TriggerDialog } from './TriggerDialog'; - import { ApiProvider } from '@backstage/core-app-api'; import { alertApiRef } from '@backstage/core-plugin-api'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { splunkOnCallApiRef } from '../../api'; +import { TriggerDialog } from './TriggerDialog'; +import { expectTriggeredIncident } from './testUtils'; describe('TriggerDialog', () => { const mockTriggerAlarmFn = jest.fn(); @@ -38,7 +38,7 @@ describe('TriggerDialog', () => { wrapInTestApp( {}} onIncidentCreated={() => {}} @@ -53,34 +53,6 @@ describe('TriggerDialog', () => { exact: false, }), ).toBeInTheDocument(); - const incidentType = getByTestId('trigger-incident-type'); - const incidentId = getByTestId('trigger-incident-id'); - const incidentDisplayName = getByTestId('trigger-incident-displayName'); - const incidentMessage = getByTestId('trigger-incident-message'); - - await act(async () => { - fireEvent.change(incidentType, { target: { value: 'CRITICAL' } }); - fireEvent.change(incidentId, { target: { value: 'incident-id' } }); - fireEvent.change(incidentDisplayName, { - target: { value: 'incident-display-name' }, - }); - fireEvent.change(incidentMessage, { - target: { value: 'incident-message' }, - }); - }); - - // Trigger incident creation button - const triggerButton = getByTestId('trigger-button'); - await act(async () => { - fireEvent.click(triggerButton); - }); - expect(mockTriggerAlarmFn).toHaveBeenCalled(); - expect(mockTriggerAlarmFn).toHaveBeenCalledWith({ - incidentType: 'CRITICAL', - incidentId: 'incident-id', - routingKey: 'Example', - incidentDisplayName: 'incident-display-name', - incidentMessage: 'incident-message', - }); + await expectTriggeredIncident('Example', getByTestId, mockTriggerAlarmFn); }); }); diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx index 82dd2da74d..dd8f0b1909 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx @@ -35,11 +35,11 @@ import { import useAsyncFn from 'react-use/lib/useAsyncFn'; import { splunkOnCallApiRef } from '../../api'; import { Alert } from '@material-ui/lab'; -import { TriggerAlarmRequest } from '../../api/types'; +import { TriggerAlarmRequest } from '../../api'; import { useApi, alertApiRef } from '@backstage/core-plugin-api'; type Props = { - team: string; + routingKey: string; showDialog: boolean; handleDialog: () => void; onIncidentCreated: () => void; @@ -76,7 +76,7 @@ const useStyles = makeStyles((theme: Theme) => ); export const TriggerDialog = ({ - team, + routingKey, showDialog, handleDialog, onIncidentCreated: onIncidentCreated, @@ -221,7 +221,7 @@ export const TriggerDialog = ({ id="details" multiline fullWidth - rows="2" + minRows="2" margin="normal" label="Incident message" variant="outlined" @@ -242,7 +242,7 @@ export const TriggerDialog = ({ variant="contained" onClick={() => handleTriggerAlarm({ - routingKey: team, + routingKey, incidentType, incidentDisplayName, incidentMessage, diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts b/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts new file mode 100644 index 0000000000..764ac94e2c --- /dev/null +++ b/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts @@ -0,0 +1,62 @@ +/* + * 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 import/no-extraneous-dependencies +import { + act, + fireEvent, + Matcher, + MatcherOptions, +} from '@testing-library/react'; + +export async function expectTriggeredIncident( + routingKey: string, + getByTestId: ( + id: Matcher, + options?: MatcherOptions | undefined, + ) => HTMLElement, + mockTriggerAlarmFn: any, +): Promise { + const incidentType = getByTestId('trigger-incident-type'); + const incidentId = getByTestId('trigger-incident-id'); + const incidentDisplayName = getByTestId('trigger-incident-displayName'); + const incidentMessage = getByTestId('trigger-incident-message'); + + await act(async () => { + fireEvent.change(incidentType, { target: { value: 'CRITICAL' } }); + fireEvent.change(incidentId, { target: { value: 'incident-id' } }); + fireEvent.change(incidentDisplayName, { + target: { value: 'incident-display-name' }, + }); + fireEvent.change(incidentMessage, { + target: { value: 'incident-message' }, + }); + }); + + // Trigger incident creation button + const triggerButton = getByTestId('trigger-button'); + await act(async () => { + fireEvent.click(triggerButton); + }); + + expect(mockTriggerAlarmFn).toHaveBeenCalled(); + expect(mockTriggerAlarmFn).toHaveBeenCalledWith({ + incidentType: 'CRITICAL', + incidentId: 'incident-id', + routingKey: routingKey, + incidentDisplayName: 'incident-display-name', + incidentMessage: 'incident-message', + }); +} diff --git a/plugins/stack-overflow-backend/CHANGELOG.md b/plugins/stack-overflow-backend/CHANGELOG.md index 50b989079a..6fdc174fb5 100644 --- a/plugins/stack-overflow-backend/CHANGELOG.md +++ b/plugins/stack-overflow-backend/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-stack-overflow-backend +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.21.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.21.2-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 0.1.7 + +### Patch Changes + +- cbe11d1e23: Tweak README +- a6d779d58a: Remove explicit default visibility at `config.d.ts` files. + + ```ts + /** + * @visibility backend + */ + ``` + +- Updated dependencies + - @backstage/cli@0.21.0 + - @backstage/config@1.0.4 + - @backstage/plugin-search-common@1.1.1 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.21.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 0.1.7-next.0 ### Patch Changes diff --git a/plugins/stack-overflow-backend/README.md b/plugins/stack-overflow-backend/README.md index acff784cc6..4bcc755404 100644 --- a/plugins/stack-overflow-backend/README.md +++ b/plugins/stack-overflow-backend/README.md @@ -15,14 +15,17 @@ stackoverflow: baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance ``` -### Stack Overflow for Teams (private stack overflow instance) +### Stack Overflow for Teams -If you have a private stack overflow instance you will need to supply an API key as well. You can read more about how to set this up by going to [Stack Overflows Help Page](https://stackoverflow.help/en/articles/4385859-stack-overflow-for-teams-api) +If you have a private Stack Overflow instance and/or a private Stack Overflow Team you will need to supply an API key. You can read more about how to set this up by going to [Stack Overflow's Help Page](https://stackoverflow.help/en/articles/4385859-stack-overflow-for-teams-api). + +A private Stack Overflow Team requires both an API key and an API Access Token. Step 3 ("Generate an Access Token via OAuth") from the previously mentioned Help Page explains the process for generating an API Access Token with no expiration. ```yaml stackoverflow: baseUrl: https://api.stackexchange.com/2.2 # alternative: your internal stack overflow instance apiKey: $STACK_OVERFLOW_API_KEY + apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN ``` ## Areas of Responsibility @@ -41,7 +44,7 @@ When you have your `packages/backend/src/plugins/search.ts` file ready to make m ```ts indexBuilder.addCollator({ - defaultRefreshIntervalSeconds: 600, + schedule, factory: StackOverflowQuestionsCollatorFactory.fromConfig(env.config, { logger: env.logger, requestParams: { diff --git a/plugins/stack-overflow-backend/api-report.md b/plugins/stack-overflow-backend/api-report.md index 11bfbaadc9..f6c7530de9 100644 --- a/plugins/stack-overflow-backend/api-report.md +++ b/plugins/stack-overflow-backend/api-report.md @@ -43,6 +43,7 @@ export type StackOverflowQuestionsCollatorFactoryOptions = { baseUrl?: string; maxPage?: number; apiKey?: string; + apiAccessToken?: string; requestParams: StackOverflowQuestionsRequestParams; logger: Logger; }; diff --git a/plugins/stack-overflow-backend/config.d.ts b/plugins/stack-overflow-backend/config.d.ts index 617524f0a3..54e517c5d8 100644 --- a/plugins/stack-overflow-backend/config.d.ts +++ b/plugins/stack-overflow-backend/config.d.ts @@ -25,9 +25,15 @@ export interface Config { baseUrl?: string; /** - * The api key to authenticate to Stack Overflow API + * The API key to authenticate to Stack Overflow API * @visibility secret */ apiKey?: string; + + /** + * The API Access Token to authenticate to Stack Overflow API + * @visibility secret + */ + apiAccessToken?: string; }; } diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index 4714c29551..5a58f54958 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow-backend", - "version": "0.1.7-next.0", + "version": "0.1.8-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts index 1aa0e5d300..ebbad99d0a 100644 --- a/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts +++ b/plugins/stack-overflow-backend/src/search/StackOverflowQuestionsCollatorFactory.ts @@ -52,6 +52,7 @@ export type StackOverflowQuestionsCollatorFactoryOptions = { baseUrl?: string; maxPage?: number; apiKey?: string; + apiAccessToken?: string; requestParams: StackOverflowQuestionsRequestParams; logger: Logger; }; @@ -67,6 +68,7 @@ export class StackOverflowQuestionsCollatorFactory protected requestParams: StackOverflowQuestionsRequestParams; private readonly baseUrl: string | undefined; private readonly apiKey: string | undefined; + private readonly apiAccessToken: string | undefined; private readonly maxPage: number | undefined; private readonly logger: Logger; public readonly type: string = 'stack-overflow'; @@ -74,6 +76,7 @@ export class StackOverflowQuestionsCollatorFactory private constructor(options: StackOverflowQuestionsCollatorFactoryOptions) { this.baseUrl = options.baseUrl; this.apiKey = options.apiKey; + this.apiAccessToken = options.apiAccessToken; this.maxPage = options.maxPage; this.requestParams = options.requestParams; this.logger = options.logger.child({ documentType: this.type }); @@ -84,6 +87,9 @@ export class StackOverflowQuestionsCollatorFactory options: StackOverflowQuestionsCollatorFactoryOptions, ) { const apiKey = config.getOptionalString('stackoverflow.apiKey'); + const apiAccessToken = config.getOptionalString( + 'stackoverflow.apiAccessToken', + ); const baseUrl = config.getOptionalString('stackoverflow.baseUrl') || 'https://api.stackexchange.com/2.2'; @@ -93,6 +99,7 @@ export class StackOverflowQuestionsCollatorFactory baseUrl, maxPage, apiKey, + apiAccessToken, }); } @@ -138,6 +145,13 @@ export class StackOverflowQuestionsCollatorFactory } const res = await fetch( `${this.baseUrl}/questions${params}${apiKeyParam}&page=${page}`, + this.apiAccessToken + ? { + headers: { + 'X-API-Access-Token': this.apiAccessToken, + }, + } + : undefined, ); const data = await res.json(); diff --git a/plugins/stack-overflow/CHANGELOG.md b/plugins/stack-overflow/CHANGELOG.md index a725475f69..26b2388e4a 100644 --- a/plugins/stack-overflow/CHANGELOG.md +++ b/plugins/stack-overflow/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/plugin-stack-overflow +## 0.1.8-next.1 + +### Patch Changes + +- a19cffbeed: Update search links to only have header as linkable text +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-home@0.4.28-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 0.1.8-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-home@0.4.28-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 0.1.7 + +### Patch Changes + +- e32d5643e3: `StackOverflowSearchResultListItem` now accept optional rank property to be able to capture rank analytics data. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-home@0.4.27 + - @backstage/config@1.0.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.1 + +## 0.1.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-home@0.4.27-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 0.1.7-next.0 ### Patch Changes diff --git a/plugins/stack-overflow/api-report.md b/plugins/stack-overflow/api-report.md index 81cfe1395b..9af9231094 100644 --- a/plugins/stack-overflow/api-report.md +++ b/plugins/stack-overflow/api-report.md @@ -44,5 +44,6 @@ export type StackOverflowQuestionsRequestParams = { export const StackOverflowSearchResultListItem: (props: { result: any; icon?: ReactNode; + rank?: number | undefined; }) => JSX.Element; ``` diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index 2b0910bbae..df1724ac16 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow", - "version": "0.1.7-next.0", + "version": "0.1.8-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -48,7 +48,7 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.test.tsx b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.test.tsx index 61e901591e..9e27149efb 100644 --- a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.test.tsx +++ b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.test.tsx @@ -15,9 +15,15 @@ */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { + MockAnalyticsApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; import { screen } from '@testing-library/react'; import { StackOverflowSearchResultListItem } from './StackOverflowSearchResultListItem'; +import { analyticsApiRef } from '@backstage/core-plugin-api'; describe('', () => { it('should render without exploding', async () => { @@ -40,4 +46,36 @@ describe('', () => { screen.getByText(/Customizing Spotify backstage UI/i).closest('a'), ).toHaveAttribute('href', 'https://stackoverflow.com/questions/7'); }); + + it('should capture analytics for rank', async () => { + const analyticsSpy = new MockAnalyticsApi(); + + await renderInTestApp( + + + , + , + ); + + await userEvent.click( + screen.getByText(/Customizing Spotify backstage UI/i), + ); + + expect(analyticsSpy.getEvents()[0]).toMatchObject({ + action: 'discover', + attributes: { to: 'https://stackoverflow.com/questions/7' }, + context: { extension: 'App', pluginId: 'root', routeRef: 'unknown' }, + subject: 'Customizing Spotify backstage UI', + value: 1, + }); + }); }); diff --git a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx index c793939593..6a2c8f5889 100644 --- a/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx +++ b/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem/StackOverflowSearchResultListItem.tsx @@ -25,25 +25,39 @@ import { Box, Chip, } from '@material-ui/core'; +import { useAnalytics } from '@backstage/core-plugin-api'; type StackOverflowSearchResultListItemProps = { result: any; // TODO(emmaindal): type to StackOverflowDocument. icon?: React.ReactNode; + rank?: number; }; export const StackOverflowSearchResultListItem = ( props: StackOverflowSearchResultListItemProps, ) => { const { location, title, text, answers, tags } = props.result; + const analytics = useAnalytics(); + + const handleClick = () => { + analytics.captureEvent('discover', title, { + attributes: { to: location }, + value: props.rank, + }); + }; return ( - + <> {props.icon && {props.icon}} + {_unescape(title)} + + } secondary={`Author: ${text}`} /> @@ -54,6 +68,6 @@ export const StackOverflowSearchResultListItem = ( - + ); }; diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md index 84a682dc8e..0b192f5ead 100644 --- a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-tech-insights-backend-module-jsonfc +## 0.1.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-tech-insights-node@0.3.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-tech-insights-common@0.2.9-next.1 + +## 0.1.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-tech-insights-common@0.2.9-next.0 + - @backstage/plugin-tech-insights-node@0.3.7-next.0 + +## 0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-tech-insights-node@0.3.6 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-tech-insights-common@0.2.8 + +## 0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-tech-insights-node@0.3.6-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/plugin-tech-insights-common@0.2.8-next.0 + ## 0.1.22-next.0 ### Patch Changes diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index 9a811bc65a..26b03afe5d 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", - "version": "0.1.22-next.0", + "version": "0.1.23-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index b39c640ddb..6001f2814d 100644 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -1,5 +1,69 @@ # @backstage/plugin-tech-insights-backend +## 0.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-tech-insights-node@0.3.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-tech-insights-common@0.2.9-next.1 + +## 0.5.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-tech-insights-common@0.2.9-next.0 + - @backstage/plugin-tech-insights-node@0.3.7-next.0 + +## 0.5.4 + +### Patch Changes + +- f12e9e5b8c: Add Documentation on 404 Errors +- 06cf8f1cf2: Add a default delay to the fact retrievers to prevent cold-start errors +- 30e43717c7: Use `HumanDuration` from `@backstage/types` +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/plugin-tech-insights-node@0.3.6 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-tech-insights-common@0.2.8 + +## 0.5.4-next.1 + +### Patch Changes + +- f12e9e5b8c: Add Documentation on 404 Errors +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/plugin-tech-insights-node@0.3.6-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-tech-insights-common@0.2.8-next.0 + ## 0.5.4-next.0 ### Patch Changes diff --git a/plugins/tech-insights-backend/README.md b/plugins/tech-insights-backend/README.md index 26dac78db5..95fa3ccda9 100644 --- a/plugins/tech-insights-backend/README.md +++ b/plugins/tech-insights-backend/README.md @@ -213,6 +213,8 @@ and modify the `techInsights.ts` file to contain a reference to the FactChecker }); ``` +NOTE: You need a Fact Checker Factory to get access to the backend routes that will allow the facts to be checked. If you don't have a Fact Checker Factory you will see 404s and potentially other errors. + To be able to run checks, you need to additionally add individual checks into your FactChecker implementation. For examples how to add these, you can check the documentation of the individual implementation of the FactChecker #### Modifying check persistence diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index 6d42c1aecc..e4ec85af2e 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend", - "version": "0.5.4-next.0", + "version": "0.5.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights-common/CHANGELOG.md b/plugins/tech-insights-common/CHANGELOG.md index 80db666774..7988e80e5d 100644 --- a/plugins/tech-insights-common/CHANGELOG.md +++ b/plugins/tech-insights-common/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-tech-insights-common +## 0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.1 + +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.2-next.0 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/types@1.0.1 + ## 0.2.8-next.0 ### Patch Changes diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index 016ccc9a2a..386b747a17 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-common", - "version": "0.2.8-next.0", + "version": "0.2.9-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights-node/CHANGELOG.md b/plugins/tech-insights-node/CHANGELOG.md index 8173dbc132..47e6a51492 100644 --- a/plugins/tech-insights-node/CHANGELOG.md +++ b/plugins/tech-insights-node/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-tech-insights-node +## 0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/plugin-tech-insights-common@0.2.9-next.1 + +## 0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/plugin-tech-insights-common@0.2.9-next.0 + +## 0.3.6 + +### Patch Changes + +- 06cf8f1cf2: Add a default delay to the fact retrievers to prevent cold-start errors +- 30e43717c7: Use `HumanDuration` from `@backstage/types` +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/types@1.0.1 + - @backstage/config@1.0.4 + - @backstage/plugin-tech-insights-common@0.2.8 + +## 0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-tech-insights-common@0.2.8-next.0 + ## 0.3.6-next.0 ### Patch Changes diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index 7300089744..6631c51f09 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-node", - "version": "0.3.6-next.0", + "version": "0.3.7-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md index f60027e045..aae7fc47be 100644 --- a/plugins/tech-insights/CHANGELOG.md +++ b/plugins/tech-insights/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-tech-insights +## 0.3.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-tech-insights-common@0.2.9-next.1 + +## 0.3.4-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-tech-insights-common@0.2.9-next.0 + +## 0.3.3 + +### Patch Changes + +- 7095e8bc03: Fixed bug when sending data by Post in `runChecks` and `runBulkChecks` functions of the `TechInsightsClient` class, the default `Content-Type` used was `plain/text` +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/types@1.0.1 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-tech-insights-common@0.2.8 + +## 0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-tech-insights-common@0.2.8-next.0 + ## 0.3.2-next.0 ### Patch Changes diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index cb9def285e..56d5389d8a 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights", - "version": "0.3.2-next.0", + "version": "0.3.4-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -56,7 +56,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index df5bee360a..53e6d3a920 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-tech-radar +## 0.5.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/theme@0.2.16 + +## 0.5.19-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/theme@0.2.16 + +## 0.5.18 + +### Patch Changes + +- 1f888af5f6: Fixed bug in Tech Radar where, on hover, the tech list quadrant would rerender and scroll top +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/theme@0.2.16 + +## 0.5.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/theme@0.2.16 + ## 0.5.18-next.0 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index a4dd1f0848..7508859a9b 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-tech-radar", "description": "A Backstage plugin that lets you display a Tech Radar for your organization", - "version": "0.5.18-next.0", + "version": "0.5.19-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -59,7 +59,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index baea1ab42f..8fa0cbcc69 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-catalog@1.6.2-next.1 + - @backstage/plugin-techdocs@1.4.1-next.1 + - @backstage/test-utils@1.2.3-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @backstage/theme@0.2.16 + +## 1.0.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/test-utils@1.2.3-next.0 + - @backstage/plugin-techdocs@1.4.1-next.0 + - @backstage/plugin-catalog@1.6.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-react@1.2.2-next.0 + +## 1.0.6 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0 + - @backstage/core-components@0.12.0 + - @backstage/core-app-api@1.2.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/plugin-catalog@1.6.1 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-search-react@1.2.1 + - @backstage/test-utils@1.2.2 + - @backstage/theme@0.2.16 + +## 1.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0-next.2 + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/test-utils@1.2.2-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog@1.6.1-next.1 + - @backstage/plugin-search-react@1.2.1-next.1 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + +## 1.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.4.0-next.1 + ## 1.0.6-next.0 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index a3da234606..892e865e20 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.6-next.0", + "version": "1.0.7-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -63,7 +63,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index b038fa202f..50eeab7df3 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,79 @@ # @backstage/plugin-techdocs-backend +## 1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/plugin-techdocs-node@1.4.3-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-catalog-common@1.0.9-next.1 + - @backstage/plugin-permission-common@0.7.2-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.4.2-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-permission-common@0.7.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.9-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-techdocs-node@1.4.3-next.0 + +## 1.4.1 + +### Patch Changes + +- a7607b5413: Replace usage of deprecataed `UrlReader.read` with `UrlReader.readUrl`. +- a6d779d58a: Remove explicit default visibility at `config.d.ts` files. + + ```ts + /** + * @visibility backend + */ + ``` + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/plugin-techdocs-node@1.4.2 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-permission-common@0.7.1 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-catalog-common@1.0.8 + - @backstage/plugin-search-common@1.1.1 + +## 1.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-techdocs-node@1.4.2-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/plugin-catalog-common@1.0.8-next.0 + - @backstage/plugin-permission-common@0.7.1-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 1.4.1-next.0 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 5e74c47d3c..1948d49271 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "1.4.1-next.0", + "version": "1.4.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -59,7 +59,7 @@ "@backstage/cli": "workspace:^", "@backstage/plugin-search-backend-node": "workspace:^", "@types/dockerode": "^3.3.0", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts index 659f4783ce..d9329151c6 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts @@ -145,7 +145,7 @@ describe('DefaultTechDocsCollatorFactory', () => { }); it('fetches from the configured catalog and tech docs services', async () => { - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('catalog'); expect(mockDiscoveryApi.getBaseUrl).toHaveBeenCalledWith('techdocs'); @@ -153,7 +153,7 @@ describe('DefaultTechDocsCollatorFactory', () => { }); it('should create documents for each tech docs search index', async () => { - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); const entity = expectedEntities[0]; documents.forEach((document, idx) => { @@ -182,7 +182,7 @@ describe('DefaultTechDocsCollatorFactory', () => { }); collator = await factory.getCollator(); - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); expect(documents[0]).toMatchObject({ @@ -200,7 +200,7 @@ describe('DefaultTechDocsCollatorFactory', () => { }); collator = await factory.getCollator(); - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); // Only 1 entity with TechDocs configured multiplied by 3 pages. @@ -222,7 +222,7 @@ describe('DefaultTechDocsCollatorFactory', () => { }); it('should create documents for each tech docs search index', async () => { - const pipeline = TestPipeline.withSubject(collator); + const pipeline = TestPipeline.fromCollator(collator); const { documents } = await pipeline.execute(); const entity = expectedEntities[0]; documents.forEach((document, idx) => { diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index c56e9cbf12..0f91a3aeb4 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/theme@0.2.16 + +## 1.0.7-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 8536e7c281: Use `app.title` from `app-config.yaml` when creating new Documentation Feedback issue. `Backstage` is the default value. +- Updated dependencies + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/theme@0.2.16 + +## 1.0.6 + +### Patch Changes + +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/integration-react@1.1.6 + - @backstage/theme@0.2.16 + +## 1.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + ## 1.0.6-next.0 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 782273e8c0..2929f3455f 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-module-addons-contrib", "description": "Plugin module for contributed TechDocs Addons", - "version": "1.0.6-next.0", + "version": "1.0.7-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,7 +42,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "@react-hookz/web": "^15.0.0", + "@react-hookz/web": "^20.0.0", "git-url-parse": "^13.0.0", "react-use": "^17.2.4" }, @@ -61,7 +61,7 @@ "@types/node": "^16.11.26", "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/ExpandableNavigation.tsx b/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/ExpandableNavigation.tsx index 5ef76187e3..802e377f42 100644 --- a/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/ExpandableNavigation.tsx +++ b/plugins/techdocs-module-addons-contrib/src/ExpandableNavigation/ExpandableNavigation.tsx @@ -61,10 +61,10 @@ type expandableNavigationLocalStorage = { */ export const ExpandableNavigationAddon = () => { const defaultValue = { expandAllNestedNavs: false }; - const [expanded, setExpanded] = + const { value: expanded, set: setExpanded } = useLocalStorageValue( EXPANDABLE_NAVIGATION_LOCAL_STORAGE, - defaultValue, + { defaultValue }, ); const [hasNavSubLevels, setHasNavSubLevels] = useState(false); diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/hooks.ts b/plugins/techdocs-module-addons-contrib/src/ReportIssue/hooks.ts index 22dc359942..800a422066 100644 --- a/plugins/techdocs-module-addons-contrib/src/ReportIssue/hooks.ts +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/hooks.ts @@ -16,7 +16,7 @@ import parseGitUrl from 'git-url-parse'; -import { useApi } from '@backstage/core-plugin-api'; +import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { replaceGithubUrlType, replaceGitLabUrlType, @@ -48,7 +48,11 @@ export const getTitle = (selection: Selection) => { return `Documentation feedback: ${text}${ellipsis}`; }; -export const getBody = (selection: Selection, markdownUrl: string) => { +export const getBody = ( + selection: Selection, + markdownUrl: string, + appTitle: string, +) => { const title = '## Documentation Feedback 📝'; const subheading = '#### The highlighted text:'; const commentHeading = '#### The comment on the text:'; @@ -61,7 +65,7 @@ export const getBody = (selection: Selection, markdownUrl: string) => { .join('\n'); const facts = [ - `Backstage URL: <${window.location.href}> \nMarkdown URL: <${markdownUrl}>`, + `${appTitle} URL: <${window.location.href}> \nMarkdown URL: <${markdownUrl}>`, ]; return `${title}\n\n ${subheading} \n\n ${highlightedTextAsQuote}\n\n ${commentHeading} \n ${commentPlaceholder}\n\n ___\n${facts}`; @@ -73,7 +77,8 @@ export const useGitTemplate = (debounceTime?: number) => { const [editLink] = useShadowRootElements([PAGE_EDIT_LINK_SELECTOR]); const url = (editLink as HTMLAnchorElement)?.href ?? ''; const scmIntegrationsApi = useApi(scmIntegrationsApiRef); - + const configApi = useApi(configApiRef); + const appTitle = configApi.getOptionalString('app.title') || 'Backstage'; if (!selection || !url) return initialTemplate; const type = scmIntegrationsApi.byUrl(url)?.type; @@ -82,7 +87,7 @@ export const useGitTemplate = (debounceTime?: number) => { return { title: getTitle(selection), - body: getBody(selection, resolveBlobUrl(url, type)), + body: getBody(selection, resolveBlobUrl(url, type), appTitle), }; }; diff --git a/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx b/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx index 6b4a055c2a..081c718c2f 100644 --- a/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx +++ b/plugins/techdocs-module-addons-contrib/src/TextSize/TextSize.test.tsx @@ -59,7 +59,7 @@ describe('TextSize', () => { expect(slider).toHaveTextContent('115%'); - let style = getComputedStyle(getByText('TEST_CONTENT')); + let style = window.getComputedStyle(getByText('TEST_CONTENT')); expect(style.getPropertyValue('--md-typeset-font-size')).toBe('18.4px'); @@ -73,7 +73,7 @@ describe('TextSize', () => { expect(slider).toHaveTextContent('100%'); - style = getComputedStyle(getByText('TEST_CONTENT')); + style = window.getComputedStyle(getByText('TEST_CONTENT')); expect(style.getPropertyValue('--md-typeset-font-size')).toBe('16px'); }); @@ -105,7 +105,7 @@ describe('TextSize', () => { expect(slider).toHaveTextContent('115%'); - let style = getComputedStyle(getByText('TEST_CONTENT')); + let style = window.getComputedStyle(getByText('TEST_CONTENT')); expect(style.getPropertyValue('--md-typeset-font-size')).toBe('18.4px'); @@ -117,7 +117,7 @@ describe('TextSize', () => { expect(slider).toHaveTextContent('100%'); - style = getComputedStyle(getByText('TEST_CONTENT')); + style = window.getComputedStyle(getByText('TEST_CONTENT')); expect(style.getPropertyValue('--md-typeset-font-size')).toBe('16px'); }); diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index cc8770c34b..65e0d267fe 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-techdocs-node +## 1.4.3-next.1 + +### Patch Changes + +- 0a61aab172: Bump dependency @azure/identity to next minor +- 37931c33ce: Upgrade to AWS SDK for Javascript v3 + + Techdocs support for AWS S3 now requires defining the AWS region to connect to. + If `techdocs.publisher.awsS3.region` is missing from the config, the AWS environment variable `AWS_REGION` will be used. + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/plugin-search-common@1.1.2-next.0 + +## 1.4.2 + +### Patch Changes + +- a7607b5413: Replace usage of deprecataed `UrlReader.read` with `UrlReader.readUrl`. +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/plugin-search-common@1.1.1 + +## 1.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/plugin-search-common@1.1.1-next.0 + ## 1.4.2-next.0 ### Patch Changes @@ -1115,7 +1170,7 @@ ### Minor Changes -- bc9d62f4f: Move the sanity checks of the publisher configurations to a dedicated `PublisherBase#getReadiness()` method instead of throwing an error when doing `Publisher.fromConfig(...)`. +- bc9d62f4f: Move the confidence checks of the publisher configurations to a dedicated `PublisherBase#getReadiness()` method instead of throwing an error when doing `Publisher.fromConfig(...)`. You should include the check when your backend to get early feedback about a potential misconfiguration: ```diff diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index eef2da8ccb..1051527e8a 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-node", "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "1.4.2-next.0", + "version": "1.4.3-next.1", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -39,7 +39,11 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@azure/identity": "^2.0.1", + "@aws-sdk/client-s3": "^3.208.0", + "@aws-sdk/credential-providers": "^3.208.0", + "@aws-sdk/lib-storage": "^3.208.0", + "@aws-sdk/types": "^3.208.0", + "@azure/identity": "^2.1.0", "@azure/storage-blob": "^12.5.0", "@backstage/backend-common": "workspace:^", "@backstage/catalog-model": "workspace:^", @@ -50,7 +54,6 @@ "@google-cloud/storage": "^6.0.0", "@trendyol-js/openstack-swift-sdk": "^0.0.5", "@types/express": "^4.17.6", - "aws-sdk": "^2.840.0", "express": "^4.17.1", "fs-extra": "10.1.0", "git-url-parse": "^13.0.0", @@ -70,6 +73,7 @@ "@types/mock-fs": "^4.13.0", "@types/recursive-readdir": "^2.2.0", "@types/supertest": "^2.0.8", + "aws-sdk-client-mock": "^2.0.0", "supertest": "^6.1.3" } } diff --git a/plugins/techdocs-node/src/stages/publish/awsS3.test.ts b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts index 52d654d159..08facf2208 100644 --- a/plugins/techdocs-node/src/stages/publish/awsS3.test.ts +++ b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts @@ -14,119 +14,31 @@ * limitations under the License. */ +import { + DeleteObjectCommand, + GetObjectCommand, + HeadBucketCommand, + HeadObjectCommand, + ListObjectsV2Command, + PutObjectCommand, + S3Client, + UploadPartCommand, +} from '@aws-sdk/client-s3'; import { getVoidLogger } from '@backstage/backend-common'; import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; +import { mockClient, AwsClientStub } from 'aws-sdk-client-mock'; import express from 'express'; import request from 'supertest'; import mockFs from 'mock-fs'; import path from 'path'; -import fs, { ReadStream } from 'fs-extra'; -import { EventEmitter } from 'events'; +import fs from 'fs-extra'; import { AwsS3Publish } from './awsS3'; import { storageRootDir } from '../../testUtils/StorageFilesMock'; +import { Readable } from 'stream'; -jest.mock('aws-sdk', () => { - const { StorageFilesMock } = require('../../testUtils/StorageFilesMock'); - const storage = new StorageFilesMock(); - - return { - __esModule: true, - Credentials: jest.requireActual('aws-sdk').Credentials, - default: { - S3: class { - constructor() { - storage.emptyFiles(); - } - - headObject({ Key }: { Key: string }) { - return { - promise: async () => { - if (!storage.fileExists(Key)) { - throw new Error('File does not exist'); - } - }, - }; - } - - getObject({ Key }: { Key: string }) { - return { - promise: async () => storage.fileExists(Key), - createReadStream: () => { - const emitter = new EventEmitter(); - process.nextTick(() => { - if (storage.fileExists(Key)) { - emitter.emit('data', Buffer.from(storage.readFile(Key))); - emitter.emit('end'); - } else { - emitter.emit( - 'error', - new Error(`The file ${Key} does not exist!`), - ); - } - }); - return emitter; - }, - }; - } - - headBucket({ Bucket }: { Bucket: string }) { - return { - promise: async () => { - if (Bucket === 'errorBucket') { - throw new Error('Bucket does not exist'); - } - return {}; - }, - }; - } - - upload({ Key, Body }: { Key: string; Body: ReadStream }) { - return { - promise: () => - new Promise(async resolve => { - const chunks = new Array(); - Body.on('data', chunk => { - chunks.push(chunk as Buffer); - }); - Body.once('end', () => { - storage.writeFile(Key, Buffer.concat(chunks)); - resolve(null); - }); - }), - }; - } - - listObjectsV2({ Bucket }: { Bucket: string }) { - return { - promise: () => { - if ( - Bucket === 'delete_stale_files_success' || - Bucket === 'delete_stale_files_error' - ) { - return Promise.resolve({ - Contents: [{ Key: 'stale_file.png' }], - }); - } - return Promise.resolve({}); - }, - }; - } - - deleteObject({ Bucket }: { Bucket: string }) { - return { - promise: () => { - if (Bucket === 'delete_stale_files_error') { - throw new Error('Message'); - } - return Promise.resolve(); - }, - }; - } - }, - }, - }; -}); +const env = process.env; +let s3Mock: AwsClientStub; const getEntityRootDir = (entity: Entity) => { const { @@ -137,6 +49,23 @@ const getEntityRootDir = (entity: Entity) => { return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name); }; +class ErrorReadable extends Readable { + errorMessage: string; + + constructor(errorMessage: string) { + super(); + this.errorMessage = errorMessage; + } + + _read() { + this.destroy(new Error(this.errorMessage)); + } + + _destroy(error: Error | null, callback: (error?: Error | null) => void) { + callback(error); + } +} + const logger = getVoidLogger(); const loggerInfoSpy = jest.spyOn(logger, 'info'); const loggerErrorSpy = jest.spyOn(logger, 'error'); @@ -219,13 +148,71 @@ describe('AwsS3Publish', () => { }; beforeEach(() => { + process.env = { ...env }; + process.env.AWS_REGION = 'us-west-2'; + mockFs({ [directory]: files, }); + + const { StorageFilesMock } = require('../../testUtils/StorageFilesMock'); + const storage = new StorageFilesMock(); + storage.emptyFiles(); + + s3Mock = mockClient(S3Client); + + s3Mock.on(HeadObjectCommand).callsFake(input => { + if (!storage.fileExists(input.Key)) { + throw new Error('File does not exist'); + } + return {}; + }); + + s3Mock.on(GetObjectCommand).callsFake(input => { + if (storage.fileExists(input.Key)) { + return { + Body: Readable.from(storage.readFile(input.Key)), + }; + } + + throw new Error(`The file ${input.Key} does not exist!`); + }); + + s3Mock.on(HeadBucketCommand).callsFake(input => { + if (input.Bucket === 'errorBucket') { + throw new Error('Bucket does not exist'); + } + return {}; + }); + + s3Mock.on(ListObjectsV2Command).callsFake(input => { + if ( + input.Bucket === 'delete_stale_files_success' || + input.Bucket === 'delete_stale_files_error' + ) { + return { + Contents: [{ Key: 'stale_file.png' }], + }; + } + return {}; + }); + + s3Mock.on(DeleteObjectCommand).callsFake(input => { + if (input.Bucket === 'delete_stale_files_error') { + throw new Error('Message'); + } + return {}; + }); + + s3Mock.on(UploadPartCommand).rejects(); + s3Mock.on(PutObjectCommand).callsFake(input => { + storage.writeFile(input.Key, input.Body); + }); }); afterEach(() => { mockFs.restore(); + process.env = env; }); describe('getReadiness', () => { @@ -476,9 +463,33 @@ describe('AwsS3Publish', () => { const fails = publisher.fetchTechDocsMetadata(invalidEntityName); + await expect(fails).rejects.toMatchObject({ + message: expect.stringContaining( + 'The file invalid/triplet/path/techdocs_metadata.json does not exist', + ), + }); + }); + + it('should return an error if the techdocs_metadata.json file cannot be read from stream', async () => { + s3Mock.on(GetObjectCommand).callsFake(_ => { + return { + Body: new ErrorReadable('No stream!'), + }; + }); + + const publisher = createPublisherFromConfig(); + + const invalidEntityName = { + namespace: 'invalid', + kind: 'triplet', + name: 'path', + }; + + const fails = publisher.fetchTechDocsMetadata(invalidEntityName); + await expect(fails).rejects.toMatchObject({ message: expect.stringMatching( - 'TechDocs metadata fetch failed; caused by Error: Unable to read stream; caused by Error: The file invalid/triplet/path/techdocs_metadata.json does not exist', + 'TechDocs metadata fetch failed; caused by Error: Unable to read stream; caused by Error: No stream!', ), }); }); @@ -597,5 +608,22 @@ describe('AwsS3Publish', () => { 'File Not Found', ); }); + + it('should return 404 if file cannot be read from stream', async () => { + s3Mock.on(GetObjectCommand).callsFake(_ => { + return { + Body: new ErrorReadable('No stream!'), + }; + }); + + const response = await request(app).get( + `/${entityTripletPath}/not-found.html`, + ); + expect(response.status).toBe(404); + + expect(Buffer.from(response.text).toString('utf8')).toEqual( + 'File Not Found', + ); + }); }); }); diff --git a/plugins/techdocs-node/src/stages/publish/awsS3.ts b/plugins/techdocs-node/src/stages/publish/awsS3.ts index 67a4157a5b..7c2405cc1c 100644 --- a/plugins/techdocs-node/src/stages/publish/awsS3.ts +++ b/plugins/techdocs-node/src/stages/publish/awsS3.ts @@ -16,9 +16,23 @@ import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError, ForwardedError } from '@backstage/errors'; -import aws, { Credentials } from 'aws-sdk'; -import { ListObjectsV2Output } from 'aws-sdk/clients/s3'; -import { CredentialsOptions } from 'aws-sdk/lib/credentials'; +import { + GetObjectCommand, + CopyObjectCommand, + DeleteObjectCommand, + HeadBucketCommand, + HeadObjectCommand, + PutObjectCommandInput, + ListObjectsV2CommandOutput, + ListObjectsV2Command, + S3Client, +} from '@aws-sdk/client-s3'; +import { + fromNodeProviderChain, + fromTemporaryCredentials, +} from '@aws-sdk/credential-providers'; +import { Upload } from '@aws-sdk/lib-storage'; +import { CredentialProvider } from '@aws-sdk/types'; import express from 'express'; import fs from 'fs-extra'; import JSON5 from 'json5'; @@ -60,7 +74,7 @@ const streamToBuffer = (stream: Readable): Promise => { }; export class AwsS3Publish implements PublisherBase { - private readonly storageClient: aws.S3; + private readonly storageClient: S3Client; private readonly bucketName: string; private readonly legacyPathCasing: boolean; private readonly logger: Logger; @@ -68,7 +82,7 @@ export class AwsS3Publish implements PublisherBase { private readonly sse?: 'aws:kms' | 'AES256'; constructor(options: { - storageClient: aws.S3; + storageClient: S3Client; bucketName: string; legacyPathCasing: boolean; logger: Logger; @@ -103,6 +117,10 @@ export class AwsS3Publish implements PublisherBase { | 'AES256' | undefined; + // AWS Region is an optional config. If missing, default AWS env variable AWS_REGION + // or AWS shared credentials file at ~/.aws/credentials will be used. + const region = config.getOptionalString('techdocs.publisher.awsS3.region'); + // Credentials is an optional config. If missing, the default ways of authenticating AWS SDK V2 will be used. // 1. AWS environment variables // https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html @@ -113,11 +131,10 @@ export class AwsS3Publish implements PublisherBase { const credentialsConfig = config.getOptionalConfig( 'techdocs.publisher.awsS3.credentials', ); - const credentials = AwsS3Publish.buildCredentials(credentialsConfig); - - // AWS Region is an optional config. If missing, default AWS env variable AWS_REGION - // or AWS shared credentials file at ~/.aws/credentials will be used. - const region = config.getOptionalString('techdocs.publisher.awsS3.region'); + const credentials = AwsS3Publish.buildCredentials( + credentialsConfig, + region, + ); // AWS endpoint is an optional config. If missing, the default endpoint is built from // the configured region. @@ -131,8 +148,9 @@ export class AwsS3Publish implements PublisherBase { 'techdocs.publisher.awsS3.s3ForcePathStyle', ); - const storageClient = new aws.S3({ - credentials, + const storageClient = new S3Client({ + customUserAgent: 'backstage-aws-techdocs-s3-publisher', + credentialDefaultProvider: () => credentials, ...(region && { region }), ...(endpoint && { endpoint }), ...(s3ForcePathStyle && { s3ForcePathStyle }), @@ -153,31 +171,42 @@ export class AwsS3Publish implements PublisherBase { }); } + private static buildStaticCredentials( + accessKeyId: string, + secretAccessKey: string, + ): CredentialProvider { + return async () => { + return Promise.resolve({ + accessKeyId, + secretAccessKey, + }); + }; + } + private static buildCredentials( config?: Config, - ): Credentials | CredentialsOptions | undefined { + region?: string, + ): CredentialProvider { if (!config) { - return undefined; + return fromNodeProviderChain(); } const accessKeyId = config.getOptionalString('accessKeyId'); const secretAccessKey = config.getOptionalString('secretAccessKey'); - let explicitCredentials: Credentials | undefined; - if (accessKeyId && secretAccessKey) { - explicitCredentials = new Credentials({ - accessKeyId, - secretAccessKey, - }); - } + const explicitCredentials: CredentialProvider = + accessKeyId && secretAccessKey + ? AwsS3Publish.buildStaticCredentials(accessKeyId, secretAccessKey) + : fromNodeProviderChain(); const roleArn = config.getOptionalString('roleArn'); if (roleArn) { - return new aws.ChainableTemporaryCredentials({ + return fromTemporaryCredentials({ masterCredentials: explicitCredentials, params: { RoleSessionName: 'backstage-aws-techdocs-s3-publisher', RoleArn: roleArn, }, + clientConfig: { region }, }); } @@ -190,9 +219,9 @@ export class AwsS3Publish implements PublisherBase { */ async getReadiness(): Promise { try { - await this.storageClient - .headBucket({ Bucket: this.bucketName }) - .promise(); + await this.storageClient.send( + new HeadBucketCommand({ Bucket: this.bucketName }), + ); this.logger.info( `Successfully connected to the AWS S3 bucket ${this.bucketName}.`, @@ -258,7 +287,7 @@ export class AwsS3Publish implements PublisherBase { const relativeFilePath = path.relative(directory, absoluteFilePath); const fileStream = fs.createReadStream(absoluteFilePath); - const params = { + const params: PutObjectCommandInput = { Bucket: this.bucketName, Key: getCloudPathForLocalPath( entity, @@ -268,10 +297,15 @@ export class AwsS3Publish implements PublisherBase { ), Body: fileStream, ...(sse && { ServerSideEncryption: sse }), - } as aws.S3.PutObjectRequest; + }; - objects.push(params.Key); - return this.storageClient.upload(params).promise(); + objects.push(params.Key!); + + const upload = new Upload({ + client: this.storageClient, + params, + }); + return upload.done(); }, absoluteFilesToUpload, { concurrencyLimit: 10 }, @@ -301,12 +335,12 @@ export class AwsS3Publish implements PublisherBase { await bulkStorageOperation( async relativeFilePath => { - return await this.storageClient - .deleteObject({ + return await this.storageClient.send( + new DeleteObjectCommand({ Bucket: this.bucketName, Key: relativeFilePath, - }) - .promise(); + }), + ); }, staleFiles, { concurrencyLimit: 10 }, @@ -334,15 +368,17 @@ export class AwsS3Publish implements PublisherBase { const entityRootDir = path.posix.join(this.bucketRootPath, entityDir); - const stream = this.storageClient - .getObject({ - Bucket: this.bucketName, - Key: `${entityRootDir}/techdocs_metadata.json`, - }) - .createReadStream(); - try { - const techdocsMetadataJson = await streamToBuffer(stream); + const resp = await this.storageClient.send( + new GetObjectCommand({ + Bucket: this.bucketName, + Key: `${entityRootDir}/techdocs_metadata.json`, + }), + ); + + const techdocsMetadataJson = await streamToBuffer( + resp.Body as Readable, + ); if (!techdocsMetadataJson) { throw new Error( `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`, @@ -384,10 +420,11 @@ export class AwsS3Publish implements PublisherBase { const fileExtension = path.extname(filePath); const responseHeaders = getHeadersForFileExtension(fileExtension); - const stream = this.storageClient - .getObject({ Bucket: this.bucketName, Key: filePath }) - .createReadStream(); try { + const resp = await this.storageClient.send( + new GetObjectCommand({ Bucket: this.bucketName, Key: filePath }), + ); + // Inject response headers for (const [headerKey, headerValue] of Object.entries( responseHeaders, @@ -395,7 +432,7 @@ export class AwsS3Publish implements PublisherBase { res.setHeader(headerKey, headerValue); } - res.send(await streamToBuffer(stream)); + res.send(await streamToBuffer(resp.Body as Readable)); } catch (err) { assertError(err); this.logger.warn( @@ -419,12 +456,12 @@ export class AwsS3Publish implements PublisherBase { const entityRootDir = path.posix.join(this.bucketRootPath, entityDir); - await this.storageClient - .headObject({ + await this.storageClient.send( + new HeadObjectCommand({ Bucket: this.bucketName, Key: `${entityRootDir}/index.html`, - }) - .promise(); + }), + ); return Promise.resolve(true); } catch (e) { return Promise.resolve(false); @@ -457,21 +494,21 @@ export class AwsS3Publish implements PublisherBase { try { this.logger.verbose(`Migrating ${file}`); - await this.storageClient - .copyObject({ + await this.storageClient.send( + new CopyObjectCommand({ Bucket: this.bucketName, CopySource: [this.bucketName, file].join('/'), Key: newPath, - }) - .promise(); + }), + ); if (removeOriginal) { - await this.storageClient - .deleteObject({ + await this.storageClient.send( + new DeleteObjectCommand({ Bucket: this.bucketName, Key: file, - }) - .promise(); + }), + ); } } catch (e) { assertError(e); @@ -490,16 +527,16 @@ export class AwsS3Publish implements PublisherBase { ): Promise { const objects: string[] = []; let nextContinuation: string | undefined; - let allObjects: ListObjectsV2Output; + let allObjects: ListObjectsV2CommandOutput; // Iterate through every file in the root of the publisher. do { - allObjects = await this.storageClient - .listObjectsV2({ + allObjects = await this.storageClient.send( + new ListObjectsV2Command({ Bucket: this.bucketName, ContinuationToken: nextContinuation, ...(prefix ? { Prefix: prefix } : {}), - }) - .promise(); + }), + ); objects.push( ...(allObjects.Contents || []).map(f => f.Key || '').filter(f => !!f), ); diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index 9418517a6c..ae1166f11d 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,51 @@ # @backstage/plugin-techdocs-react +## 1.0.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/version-bridge@1.0.3-next.0 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/catalog-model@1.1.4-next.1 + +## 1.0.7-next.0 + +### Patch Changes + +- cb716004ef: Internal refactor to improve tests +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/version-bridge@1.0.2 + +## 1.0.6 + +### Patch Changes + +- 7d1352d459: Analytics events captured within the `` now include the conventional `entityRef` context value, associating those events with their corresponding entity. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/version-bridge@1.0.2 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/config@1.0.4 + +## 1.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/version-bridge@1.0.1 + ## 1.0.6-next.0 ### Patch Changes diff --git a/plugins/techdocs-react/api-report.md b/plugins/techdocs-react/api-report.md index d04b852d07..09da4e012c 100644 --- a/plugins/techdocs-react/api-report.md +++ b/plugins/techdocs-react/api-report.md @@ -173,7 +173,7 @@ export const useShadowRootElements: < ) => TReturnedElement[]; // @public -export const useShadowRootSelection: (wait?: number) => Selection | null; +export const useShadowRootSelection: (waitMillis?: number) => Selection | null; // @public export const useTechDocsAddons: () => { diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index d7ff40bed8..3bd8646517 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-react", "description": "Shared frontend utilities for TechDocs and Addons", - "version": "1.0.6-next.0", + "version": "1.0.7-next.1", "publishConfig": { "access": "public", "alphaTypes": "dist/index.alpha.d.ts", diff --git a/plugins/techdocs-react/src/context.test.tsx b/plugins/techdocs-react/src/context.test.tsx index 6cc5537d9e..4800204f36 100644 --- a/plugins/techdocs-react/src/context.test.tsx +++ b/plugins/techdocs-react/src/context.test.tsx @@ -19,9 +19,13 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { TestApiProvider } from '@backstage/test-utils'; +import { MockAnalyticsApi, TestApiProvider } from '@backstage/test-utils'; import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; -import { configApiRef } from '@backstage/core-plugin-api'; +import { + analyticsApiRef, + configApiRef, + useAnalytics, +} from '@backstage/core-plugin-api'; import { techdocsApiRef } from './api'; import { useTechDocsReaderPage, TechDocsReaderPageProvider } from './context'; @@ -60,6 +64,8 @@ const configApiMock = { getOptionalBoolean: jest.fn().mockReturnValue(undefined), }; +const analyticsApiMock = new MockAnalyticsApi(); + const wrapper = ({ entityRef = { kind: mockEntityMetadata.kind, @@ -74,6 +80,7 @@ const wrapper = ({ { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should set title', async () => { const { result, waitForNextUpdate } = renderHook( () => useTechDocsReaderPage(), @@ -157,4 +168,21 @@ describe('useTechDocsReaderPage', () => { expect(result.current.entityRef).toStrictEqual(caseSensitiveEntityRef); }); + + it('entityRef provided as analytics context', async () => { + const { waitForNextUpdate } = renderHook( + () => useAnalytics().captureEvent('action', 'subject'), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + action: 'action', + subject: 'subject', + context: { + entityRef: 'component:default/test', + }, + }); + }); }); diff --git a/plugins/techdocs-react/src/context.tsx b/plugins/techdocs-react/src/context.tsx index 66faf858fa..2a5b151824 100644 --- a/plugins/techdocs-react/src/context.tsx +++ b/plugins/techdocs-react/src/context.tsx @@ -33,7 +33,11 @@ import { createVersionedValueMap, } from '@backstage/version-bridge'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { + AnalyticsContext, + configApiRef, + useApi, +} from '@backstage/core-plugin-api'; import { techdocsApiRef } from './api'; import { TechDocsEntityMetadata, TechDocsMetadata } from './types'; @@ -141,9 +145,13 @@ export const TechDocsReaderPageProvider = memo( const versionedValue = createVersionedValueMap({ 1: value }); return ( - - {children instanceof Function ? children(value) : children} - + + + {children instanceof Function ? children(value) : children} + + ); }, (prevProps, nextProps) => { diff --git a/plugins/techdocs-react/src/hooks.ts b/plugins/techdocs-react/src/hooks.ts index 560c548156..e8cc7bddc8 100644 --- a/plugins/techdocs-react/src/hooks.ts +++ b/plugins/techdocs-react/src/hooks.ts @@ -57,10 +57,10 @@ const isValidSelection = (newSelection: Selection) => { }; /** - * Hook for retreiving a selection within the ShadowRoot. + * Hook for retrieving a selection within the ShadowRoot. * @public */ -export const useShadowRootSelection = (wait: number = 0) => { +export const useShadowRootSelection = (waitMillis: number = 0) => { const shadowRoot = useShadowRoot(); const [selection, setSelection] = useState(null); const handleSelectionChange = useMemo( @@ -78,8 +78,8 @@ export const useShadowRootSelection = (wait: number = 0) => { } else { setSelection(null); } - }, wait), - [shadowRoot, setSelection, wait], + }, waitMillis), + [shadowRoot, setSelection, waitMillis], ); useEffect(() => { diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 9a6c681a82..c3c41837af 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,121 @@ # @backstage/plugin-techdocs +## 1.4.1-next.1 + +### Patch Changes + +- d3fea4ae0a: Internal fixes to avoid implicit usage of globals +- a19cffbeed: Update search links to only have header as linkable text +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/plugin-search-react@1.2.2-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/integration-react@1.1.7-next.1 + - @backstage/plugin-techdocs-react@1.0.7-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.1 + +## 1.4.1-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/plugin-techdocs-react@1.0.7-next.0 + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/integration-react@1.1.7-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.2-next.0 + - @backstage/plugin-search-react@1.2.2-next.0 + +## 1.4.0 + +### Minor Changes + +- 5691baea69: Add ability to configure filters when using EntityListDocsGrid + + The following example will render two sections of cards grid: + + - One section for documentations tagged as `recommended` + - One section for documentations tagged as `runbook` + + ```js + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + { + title: "RunBooks Documentation", + filterPredicate: entity => + entity?.metadata?.tags?.includes('runbook') ?? false, + } + ]}} /> + ``` + +- 63705e73d9: Hide document description if not provided +- 847fc588a6: Updated TechDocs header to include label for source code icon and updated label to reflect Kind name + +### Patch Changes + +- 9e4d8e6198: Fix logic bug that broke techdocs-cli-embedded-app +- e92aa15f01: Bumped `canvas` dependency to the latest version, which has better Node.js v18 support. +- cbe11d1e23: Tweak README +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- c1784a4980: Replaces in-code uses of `GitHub` with `Github` and deprecates old versions. +- 3a1a999b7b: Include query parameters when navigating to relative links in documents +- bd2aab4726: An analytics event matching the semantics of the `click` action is now captured when users click links within a TechDocs document. +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-techdocs-react@1.0.6 + - @backstage/integration-react@1.1.6 + - @backstage/plugin-search-react@1.2.1 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + - @backstage/plugin-search-common@1.1.1 + +## 1.4.0-next.2 + +### Patch Changes + +- e92aa15f01: Bumped `canvas` dependency to the latest version, which has better Node.js v18 support. +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + - @backstage/integration-react@1.1.6-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + - @backstage/plugin-search-common@1.1.1-next.0 + - @backstage/plugin-search-react@1.2.1-next.1 + - @backstage/plugin-techdocs-react@1.0.6-next.1 + +## 1.4.0-next.1 + +### Patch Changes + +- 9e4d8e6198: Fix logic bug that broke techdocs-cli-embedded-app + ## 1.4.0-next.0 ### Minor Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 05b841fd0c..8ac338a656 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "1.4.0-next.0", + "version": "1.4.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -76,9 +76,9 @@ "@types/dompurify": "^2.2.2", "@types/event-source-polyfill": "^1.0.0", "@types/node": "^16.11.26", - "canvas": "^2.6.1", + "canvas": "^2.10.2", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist", diff --git a/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx b/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx index effd8fe0c5..acaf01b318 100644 --- a/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx +++ b/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx @@ -103,10 +103,10 @@ describe('Entity List Docs Grid', () => { }, ); - expect(await screen.queryByText('All Documentation')).toBeInTheDocument(); - expect(await screen.queryByText('Documentation #1')).toBeInTheDocument(); - expect(await screen.queryByText('Documentation #2')).toBeInTheDocument(); - expect(await screen.queryByTestId('doc-not-found')).not.toBeInTheDocument(); + expect(screen.getByText('All Documentation')).toBeInTheDocument(); + expect(screen.getByText('Documentation #1')).toBeInTheDocument(); + expect(screen.getByText('Documentation #2')).toBeInTheDocument(); + expect(screen.queryByTestId('doc-not-found')).not.toBeInTheDocument(); }); it('should render only filtered entities with filtering', async () => { @@ -131,14 +131,10 @@ describe('Entity List Docs Grid', () => { }, ); - expect( - await screen.queryByText('Curated Documentation'), - ).toBeInTheDocument(); - expect(await screen.queryByText('Documentation #1')).toBeInTheDocument(); - expect( - await screen.queryByText('Documentation #2'), - ).not.toBeInTheDocument(); - expect(await screen.queryByTestId('doc-not-found')).not.toBeInTheDocument(); + expect(screen.getByText('Curated Documentation')).toBeInTheDocument(); + expect(screen.getByText('Documentation #1')).toBeInTheDocument(); + expect(screen.queryByText('Documentation #2')).not.toBeInTheDocument(); + expect(screen.queryByTestId('doc-not-found')).not.toBeInTheDocument(); }); it('should render nothing with filtering yielding no result', async () => { @@ -163,16 +159,10 @@ describe('Entity List Docs Grid', () => { }, ); - expect( - await screen.queryByText('Curated Documentation'), - ).not.toBeInTheDocument(); - expect( - await screen.queryByText('Documentation #1'), - ).not.toBeInTheDocument(); - expect( - await screen.queryByText('Documentation #2'), - ).not.toBeInTheDocument(); - expect(await screen.queryByTestId('doc-not-found')).not.toBeInTheDocument(); + expect(screen.queryByText('Curated Documentation')).not.toBeInTheDocument(); + expect(screen.queryByText('Documentation #1')).not.toBeInTheDocument(); + expect(screen.queryByText('Documentation #2')).not.toBeInTheDocument(); + expect(screen.queryByTestId('doc-not-found')).not.toBeInTheDocument(); }); it('should render an error without any documentation and without filtering', async () => { @@ -189,15 +179,9 @@ describe('Entity List Docs Grid', () => { }, ); - expect( - await screen.queryByText('All Documentation'), - ).not.toBeInTheDocument(); - expect( - await screen.queryByText('Documentation #1'), - ).not.toBeInTheDocument(); - expect( - await screen.queryByText('Documentation #2'), - ).not.toBeInTheDocument(); - expect(await screen.queryByTestId('doc-not-found')).toBeInTheDocument(); + expect(screen.queryByText('All Documentation')).not.toBeInTheDocument(); + expect(screen.queryByText('Documentation #1')).not.toBeInTheDocument(); + expect(screen.queryByText('Documentation #2')).not.toBeInTheDocument(); + expect(screen.getByTestId('doc-not-found')).toBeInTheDocument(); }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index e886454594..9babff0c54 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -21,7 +21,7 @@ import { useTheme, useMediaQuery } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; import { CompoundEntityRef } from '@backstage/catalog-model'; -import { useApi } from '@backstage/core-plugin-api'; +import { useAnalytics, useApi } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { @@ -63,6 +63,7 @@ export const useTechDocsReaderDom = ( const isMobileMedia = useMediaQuery(MOBILE_MEDIA_QUERY); const sanitizerTransformer = useSanitizerTransformer(); const stylesTransformer = useStylesTransformer(); + const analytics = useAnalytics(); const techdocsStorageApi = useApi(techdocsStorageApiRef); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); @@ -177,6 +178,12 @@ export const useTechDocsReaderDom = ( const parsedUrl = new URL(url); const fullPath = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`; + // capture link clicks within documentation + const linkText = + (event.target as HTMLAnchorElement | undefined)?.innerText || url; + const to = url.replace(window.location.origin, ''); + analytics.captureEvent('click', linkText, { attributes: { to } }); + // hash exists when anchor is clicked on secondary sidebar if (parsedUrl.hash) { if (modifierActive) { @@ -219,7 +226,7 @@ export const useTechDocsReaderDom = ( onLoaded: () => {}, }), ]), - [theme, navigate], + [theme, navigate, analytics], ); useEffect(() => { diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx index e76cb1d173..dcb75f4cc4 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageHeader/TechDocsReaderPageHeader.tsx @@ -17,6 +17,7 @@ import React, { PropsWithChildren, useEffect } from 'react'; import Helmet from 'react-helmet'; +import { Grid } from '@material-ui/core'; import { Skeleton } from '@material-ui/lab'; import CodeIcon from '@material-ui/icons/Code'; @@ -36,6 +37,8 @@ import { RELATION_OWNED_BY, CompoundEntityRef } from '@backstage/catalog-model'; import { Header, HeaderLabel } from '@backstage/core-components'; import { useRouteRef, configApiRef, useApi } from '@backstage/core-plugin-api'; +import { capitalize } from 'lodash'; + import { rootRouteRef } from '../../../routes'; const skeleton = ; @@ -81,7 +84,7 @@ export const TechDocsReaderPageHeader = ( setSubtitle(() => { let { site_description } = metadata; if (!site_description || site_description === 'None') { - site_description = 'Home'; + site_description = ''; } return site_description; }); @@ -102,7 +105,7 @@ export const TechDocsReaderPageHeader = ( const labels = ( <> - - + + + + + Source + + } + url={locationMetadata.target} /> ) : null} @@ -155,7 +165,7 @@ export const TechDocsReaderPageHeader = ( type="Documentation" typeLink={docsRootLink} title={title || skeleton} - subtitle={subtitle || skeleton} + subtitle={subtitle === '' ? undefined : subtitle || skeleton} > {tabTitle} diff --git a/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts b/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts index 4613df374d..9c4ee08346 100644 --- a/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts +++ b/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts @@ -20,7 +20,7 @@ import { lightTheme } from '@backstage/theme'; import { waitFor } from '@testing-library/react'; const clipboardSpy = jest.fn(); -Object.defineProperty(navigator, 'clipboard', { +Object.defineProperty(window.navigator, 'clipboard', { value: { writeText: clipboardSpy, }, diff --git a/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx b/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx index 25a1d67f65..6adb4252d3 100644 --- a/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx +++ b/plugins/techdocs/src/reader/transformers/copyToClipboard.tsx @@ -49,7 +49,7 @@ const CopyToClipboardButton = ({ text }: CopyToClipboardButtonProps) => { const [open, setOpen] = useState(false); const handleClick = useCallback(() => { - navigator.clipboard.writeText(text); + window.navigator.clipboard.writeText(text); setOpen(true); }, [text]); diff --git a/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx b/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx index 430165bf8b..c7755e8733 100644 --- a/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx @@ -81,6 +81,15 @@ export const TechDocsSearchResultListItem = ( }); }; + const LinkWrapper = ({ children }: PropsWithChildren<{}>) => + asLink ? ( + + {children} + + ) : ( + <>{children} + ); + const TextItem = () => { const resultTitle = highlight?.fields.title ? ( - {resultTitle} | {entityTitle ?? resultName} docs - - ) + + {title ? ( + title + ) : ( + <> + {resultTitle} | {entityTitle ?? resultName} docs + + )} + } secondary={ ) => - asLink ? ( - - {children} - - ) : ( - <>{children} - ); - const ListItemWrapper = ({ children }: PropsWithChildren<{}>) => asListItem ? ( <> @@ -172,10 +174,8 @@ export const TechDocsSearchResultListItem = ( ); return ( - - - - - + + + ); }; diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index f4827591df..5d674fa1c1 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-todo-backend +## 0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/integration@1.4.1-next.1 + - @backstage/catalog-client@1.2.0-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.1.36-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/catalog-client@1.2.0-next.0 + - @backstage/backend-common@0.16.1-next.0 + - @backstage/integration@1.4.1-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.1.35 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/integration@1.4.0 + - @backstage/catalog-model@1.1.3 + - @backstage/catalog-client@1.1.2 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.1.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/catalog-client@1.1.2-next.0 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/integration@1.4.0-next.0 + ## 0.1.35-next.0 ### Patch Changes diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index ae227478b6..0c627b7c20 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo-backend", "description": "A Backstage backend plugin that lets you browse TODO comments in your source code", - "version": "0.1.35-next.0", + "version": "0.1.36-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -45,7 +45,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.3" }, "files": [ diff --git a/plugins/todo-backend/src/service/TodoReaderService.test.ts b/plugins/todo-backend/src/service/TodoReaderService.test.ts index 8077e2ddb9..3f68636a05 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.test.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.test.ts @@ -58,7 +58,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked { if (entity) { mock.getEntityByRef.mockReturnValue(entity); } - return mock; + return mock as Partial> as jest.Mocked; } function mockTodoReader(items?: TodoItem[]): jest.Mocked { diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md index c97576555d..b45bb7d3f8 100644 --- a/plugins/todo/CHANGELOG.md +++ b/plugins/todo/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-todo +## 0.2.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.2.14-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.2.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.2.13-next.0 ### Patch Changes diff --git a/plugins/todo/package.json b/plugins/todo/package.json index c5cd8df216..4bbca2a6e2 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo", "description": "A Backstage plugin that lets you browse TODO comments in your source code", - "version": "0.2.13-next.0", + "version": "0.2.14-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -54,7 +54,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 54a75f3888..c931f45782 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-user-settings-backend +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/plugin-auth-node@0.2.8-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/plugin-auth-node@0.2.8-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/catalog-model@1.1.3 + - @backstage/plugin-auth-node@0.2.7 + - @backstage/types@1.0.1 + - @backstage/errors@1.1.3 + +## 0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/plugin-auth-node@0.2.7-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/types@1.0.1-next.0 + ## 0.1.2-next.0 ### Patch Changes diff --git a/plugins/user-settings-backend/migrations/20220824183000_init.js b/plugins/user-settings-backend/migrations/20220824183000_init.js index a519969a98..0e99a76f93 100644 --- a/plugins/user-settings-backend/migrations/20220824183000_init.js +++ b/plugins/user-settings-backend/migrations/20220824183000_init.js @@ -14,6 +14,8 @@ * limitations under the License. */ +// @ts-check + /** * @param {import('knex').Knex} knex */ diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index 5015b60456..a405cbba9a 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings-backend", "description": "The Backstage backend plugin to manage user settings", - "version": "0.1.2-next.0", + "version": "0.1.3-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index d73d533cba..970e0efd76 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-user-settings +## 0.6.0-next.1 + +### Patch Changes + +- c3fa90e184: Updated dependency `zen-observable` to `^0.10.0`. +- Updated dependencies + - @backstage/core-app-api@1.2.1-next.1 + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/types@1.0.2-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.6.0-next.0 + +### Minor Changes + +- 29bdda5442: Added the ability to fully customize settings page. Deprecated UserSettingsTab in favour of SettingsLayout.Route + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 19356df560: Updated dependency `zen-observable` to `^0.9.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-app-api@1.2.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/types@1.0.2-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.5.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-app-api@1.2.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/types@1.0.1 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.5.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-app-api@1.2.0-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.1-next.0 + ## 0.5.1-next.0 ### Patch Changes diff --git a/plugins/user-settings/README.md b/plugins/user-settings/README.md index ce07d76257..9a99a69b36 100644 --- a/plugins/user-settings/README.md +++ b/plugins/user-settings/README.md @@ -16,7 +16,7 @@ for installation instructions. Add the item to the Sidebar: -```ts +```tsx import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; @@ -28,7 +28,7 @@ import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; Add the page to the App routing: -```ts +```tsx import { UserSettingsPage } from '@backstage/plugin-user-settings'; const AppRoutes = () => ( @@ -46,7 +46,7 @@ By default, the plugin provides a list of configured authentication providers fe If you want to supply your own custom list of Authentication Providers, use the `providerSettings` prop: -```ts +```tsx const MyAuthProviders = () => ( @@ -71,20 +71,20 @@ const AppRoutes = () => ( By default, the plugin renders 3 tabs of settings; GENERAL, AUTHENTICATION PROVIDERS, and FEATURE FLAGS. If you want to add more options for your users, -just pass the extra tabs using `UserSettingsTab` components as children of the `UserSettingsPage` route. +just pass the extra tabs using `SettingsLayout.Route` components as children of the `UserSettingsPage` route. The path is in this case a child of the settings path, in the example below it would be `/settings/advanced` so that you can easily link to it. ```tsx import { + SettingsLayout, UserSettingsPage, - UserSettingsTab, } from '@backstage/plugin-user-settings'; }> - + - + ; ``` @@ -93,3 +93,40 @@ make sure you use a similar component structure as the other tabs. You can take a look at [the example extra tab](https://github.com/backstage/backstage/blob/master/packages/app/src/components/advancedSettings/AdvancedSettings.tsx) we have created in Backstage's demo app. + +To change the layout altogether, create a custom page in `packages/app/src/components/user-settings/SettingsPage.tsx`: + +```tsx +import React from 'react'; +import { + SettingsLayout, + UserSettingsGeneral, +} from '@backstage/plugin-user-settings'; +import { AdvancedSettings } from './advancedSettings'; + +export const settingsPage = ( + + + + + + + + +); +``` + +Now register the new settings page in `packages/app/src/App.tsx`: + +```diff ++ import {settingsPage} from './components/settings/settingsPage'; + +const routes = ( + +- } /> ++ }> ++ {settingsPage} ++ + +); +``` diff --git a/plugins/user-settings/api-report.md b/plugins/user-settings/api-report.md index c51d4f992a..dd8f9428ca 100644 --- a/plugins/user-settings/api-report.md +++ b/plugins/user-settings/api-report.md @@ -18,10 +18,12 @@ import { Observable } from '@backstage/types'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; +import { TabProps } from '@material-ui/core'; // @public (undocumented) export const DefaultProviderSettings: (props: { @@ -43,7 +45,33 @@ export const Router: (props: { providerSettings?: JSX.Element }) => JSX.Element; export const Settings: (props: { icon?: IconComponent }) => JSX.Element; // @public (undocumented) -export const USER_SETTINGS_TAB_KEY = 'user-settings.tab'; +export const SettingsLayout: { + (props: SettingsLayoutProps): JSX.Element; + Route: (props: SettingsLayoutRouteProps) => null; +}; + +// @public (undocumented) +export type SettingsLayoutProps = { + title?: string; + subtitle?: string; + children?: React_2.ReactNode; +}; + +// @public (undocumented) +export type SettingsLayoutRouteProps = { + path: string; + title: string; + children: JSX.Element; + tabProps?: TabProps< + React_2.ElementType, + { + component?: React_2.ElementType; + } + >; +}; + +// @public @deprecated (undocumented) +export const USER_SETTINGS_TAB_KEY = 'plugin.user-settings.settingsLayoutRoute'; // @public (undocumented) export const UserSettingsAppearanceCard: () => JSX.Element; @@ -116,10 +144,10 @@ export class UserSettingsStorage implements StorageApi { snapshot(key: string): StorageValueSnapshot; } -// @public +// @public @deprecated export const UserSettingsTab: (props: UserSettingsTabProps) => JSX.Element; -// @public (undocumented) +// @public @deprecated (undocumented) export type UserSettingsTabProps = PropsWithChildren<{ path: string; title: string; diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 744cf40eec..c20b29aca6 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings", "description": "A Backstage plugin that provides a settings page", - "version": "0.5.1-next.0", + "version": "0.6.0-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -43,7 +43,7 @@ "@material-ui/lab": "4.0.0-alpha.57", "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4", - "zen-observable": "^0.8.15" + "zen-observable": "^0.10.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", @@ -58,7 +58,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/user-settings/src/components/SettingsPage.test.tsx b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx similarity index 63% rename from plugins/user-settings/src/components/SettingsPage.test.tsx rename to plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx index 01810bcc8e..d4560bdd06 100644 --- a/plugins/user-settings/src/components/SettingsPage.test.tsx +++ b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx @@ -16,39 +16,53 @@ import React from 'react'; import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; -import { SettingsPage } from './SettingsPage'; -import { UserSettingsTab } from './UserSettingsTab'; +import { DefaultSettingsPage } from './DefaultSettingsPage'; +import { UserSettingsTab } from '../UserSettingsTab'; +import { useOutlet } from 'react-router'; +import { SettingsLayout } from '../SettingsLayout'; jest.mock('react-router', () => ({ ...jest.requireActual('react-router'), useOutlet: jest.fn().mockReturnValue(undefined), })); -import { useOutlet } from 'react-router'; - -describe('', () => { +describe('', () => { beforeEach(() => { (useOutlet as jest.Mock).mockReset(); }); it('should render the settings page with 3 tabs', async () => { const { container } = await renderWithEffects( - wrapInTestApp(), + wrapInTestApp(), ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); expect(tabs).toHaveLength(3); }); - it('should render the settings page with 4 tabs when extra tabs are provided', async () => { + it('should render the settings page with 4 tabs when extra tabs are provided via UserSettingsTab', async () => { const advancedTabRoute = (
Advanced settings
); - (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); const { container } = await renderWithEffects( - wrapInTestApp(), + wrapInTestApp(), + ); + + const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); + expect(tabs).toHaveLength(4); + expect(tabs[3].textContent).toEqual('Advanced'); + }); + + it('should render the settings page with 4 tabs when extra tabs are provided via SettingsLayout.Route', async () => { + const advancedTabRoute = ( + +
Advanced settings
+
+ ); + const { container } = await renderWithEffects( + wrapInTestApp(), ); const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); diff --git a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx new file mode 100644 index 0000000000..6b5b63d6bd --- /dev/null +++ b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { UserSettingsAuthProviders } from '../AuthProviders'; +import { UserSettingsFeatureFlags } from '../FeatureFlags'; +import { UserSettingsGeneral } from '../General'; +import { SettingsLayout, SettingsLayoutRouteProps } from '../SettingsLayout'; + +/** + * @public + */ +export const DefaultSettingsPage = (props: { + tabs?: React.ReactElement[]; + providerSettings?: JSX.Element; +}) => { + const { providerSettings, tabs } = props; + + return ( + + + + + + + + + + + {tabs} + + ); +}; diff --git a/plugins/user-settings/src/components/DefaultSettingsPage/index.ts b/plugins/user-settings/src/components/DefaultSettingsPage/index.ts new file mode 100644 index 0000000000..34dcb9340d --- /dev/null +++ b/plugins/user-settings/src/components/DefaultSettingsPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { DefaultSettingsPage } from './DefaultSettingsPage'; diff --git a/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx new file mode 100644 index 0000000000..9855fdd048 --- /dev/null +++ b/plugins/user-settings/src/components/SettingsLayout/SettingsLayout.tsx @@ -0,0 +1,82 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { TabProps } from '@material-ui/core'; +import { + Header, + Page, + RoutedTabs, + useSidebarPinState, +} from '@backstage/core-components'; +import { + attachComponentData, + useElementFilter, +} from '@backstage/core-plugin-api'; + +/** @public */ +export type SettingsLayoutRouteProps = { + path: string; + title: string; + children: JSX.Element; + tabProps?: TabProps; +}; + +export const LAYOUT_DATA_KEY = 'plugin.user-settings.settingsLayout'; +export const LAYOUT_ROUTE_DATA_KEY = 'plugin.user-settings.settingsLayoutRoute'; + +const Route: (props: SettingsLayoutRouteProps) => null = () => null; +attachComponentData(Route, LAYOUT_ROUTE_DATA_KEY, true); + +// This causes all mount points that are discovered within this route to use the path of the route itself +attachComponentData(Route, 'core.gatherMountPoints', true); + +/** @public */ +export type SettingsLayoutProps = { + title?: string; + subtitle?: string; + children?: React.ReactNode; +}; + +/** + * @public + */ +export const SettingsLayout = (props: SettingsLayoutProps) => { + const { title, children } = props; + const { isMobile } = useSidebarPinState(); + + const routes = useElementFilter(children, elements => + elements + .selectByComponentData({ + key: LAYOUT_ROUTE_DATA_KEY, + withStrictError: + 'Child of SettingsLayout must be an SettingsLayout.Route', + }) + .getElements() + .map(child => child.props), + ); + + return ( + + {!isMobile &&
} + + + ); +}; + +attachComponentData(SettingsLayout, LAYOUT_DATA_KEY, true); + +SettingsLayout.Route = Route; diff --git a/plugins/user-settings/src/components/SettingsLayout/index.ts b/plugins/user-settings/src/components/SettingsLayout/index.ts new file mode 100644 index 0000000000..6e610f8078 --- /dev/null +++ b/plugins/user-settings/src/components/SettingsLayout/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { + SettingsLayoutProps, + SettingsLayoutRouteProps, +} from './SettingsLayout'; +export { SettingsLayout } from './SettingsLayout'; diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx deleted file mode 100644 index 9803bf4761..0000000000 --- a/plugins/user-settings/src/components/SettingsPage.tsx +++ /dev/null @@ -1,72 +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 { - Header, - Page, - TabbedLayout, - useSidebarPinState, -} from '@backstage/core-components'; -import React from 'react'; -import { useOutlet } from 'react-router'; -import { useElementFilter } from '@backstage/core-plugin-api'; -import { UserSettingsAuthProviders } from './AuthProviders'; -import { UserSettingsFeatureFlags } from './FeatureFlags'; -import { UserSettingsGeneral } from './General'; -import { USER_SETTINGS_TAB_KEY, UserSettingsTabProps } from './UserSettingsTab'; - -/** - * @public - */ -export const SettingsPage = (props: { providerSettings?: JSX.Element }) => { - const { providerSettings } = props; - const { isMobile } = useSidebarPinState(); - const outlet = useOutlet(); - - const tabs = useElementFilter(outlet, elements => - elements - .selectByComponentData({ - key: USER_SETTINGS_TAB_KEY, - }) - .getElements(), - ); - - return ( - - {!isMobile &&
} - - - - - - - - - - - - {tabs.map((child, i) => ( - - {child} - - ))} - - - ); -}; diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx new file mode 100644 index 0000000000..b6277f0b59 --- /dev/null +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx @@ -0,0 +1,106 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { SettingsPage } from './SettingsPage'; +import { UserSettingsTab } from '../UserSettingsTab'; +import { useOutlet } from 'react-router'; +import { SettingsLayout } from '../SettingsLayout'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useOutlet: jest.fn().mockReturnValue(undefined), +})); + +describe('', () => { + beforeEach(() => { + (useOutlet as jest.Mock).mockReset(); + }); + + it('should render the default settings page with 3 tabs', async () => { + const { container } = await renderWithEffects( + wrapInTestApp(), + ); + + const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); + expect(tabs).toHaveLength(3); + }); + + it('should render the default settings page with 4 tabs when extra tabs are provided via UserSettingsTab', async () => { + const advancedTabRoute = ( + +
Advanced settings
+
+ ); + (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); + const { container } = await renderWithEffects( + wrapInTestApp(), + ); + + const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); + expect(tabs).toHaveLength(4); + expect(tabs[3].textContent).toEqual('Advanced'); + }); + + it('should render the default settings page with 4 tabs when extra tabs are provided via SettingsLayout.Route', async () => { + const advancedTabRoute = ( + +
Advanced settings
+
+ ); + (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); + const { container } = await renderWithEffects( + wrapInTestApp(), + ); + + const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); + expect(tabs).toHaveLength(4); + expect(tabs[3].textContent).toEqual('Advanced'); + const user = userEvent.setup(); + await user.click(screen.getByText(/Advanced/i)); + const content = container.querySelectorAll('article'); + expect(content[0].textContent).toEqual('Advanced settings'); + }); + + it('should render the custom settings page when custom layout is provided', async () => { + const customLayout = ( + + +
User settings
+
+ +
Advanced settings
+
+
+ ); + (useOutlet as jest.Mock).mockReturnValue(customLayout); + const { container } = await renderWithEffects( + wrapInTestApp(), + ); + + const tabs = container.querySelectorAll('[class*=MuiTabs-root] button'); + expect(tabs).toHaveLength(2); + expect(tabs[0].textContent).toEqual('General'); + expect(tabs[1].textContent).toEqual('Advanced'); + const user = userEvent.setup(); + await user.click(screen.getByText(/Advanced/i)); + const content = container.querySelectorAll('article'); + expect(content[0].textContent).toEqual('Advanced settings'); + }); +}); diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx new file mode 100644 index 0000000000..eec386d60e --- /dev/null +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useOutlet } from 'react-router'; +import React from 'react'; +import { DefaultSettingsPage } from '../DefaultSettingsPage'; +import { useElementFilter } from '@backstage/core-plugin-api'; +import { + SettingsLayoutProps, + SettingsLayoutRouteProps, +} from '../SettingsLayout'; +import { + LAYOUT_DATA_KEY, + LAYOUT_ROUTE_DATA_KEY, +} from '../SettingsLayout/SettingsLayout'; + +/** @public */ +export const SettingsPage = (props: { providerSettings?: JSX.Element }) => { + const { providerSettings } = props; + const outlet = useOutlet(); + const layout = useElementFilter(outlet, elements => + elements + .selectByComponentData({ + key: LAYOUT_DATA_KEY, + }) + .getElements(), + ); + const tabs = useElementFilter(outlet, elements => + elements + .selectByComponentData({ + key: LAYOUT_ROUTE_DATA_KEY, + }) + .getElements(), + ); + + return ( + <> + {(layout.length !== 0 && layout) || ( + + )} + + ); +}; diff --git a/plugins/user-settings/src/components/SettingsPage/index.ts b/plugins/user-settings/src/components/SettingsPage/index.ts new file mode 100644 index 0000000000..e957a1bf09 --- /dev/null +++ b/plugins/user-settings/src/components/SettingsPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { SettingsPage } from './SettingsPage'; diff --git a/plugins/user-settings/src/components/UserSettingsTab/UserSettingsTab.tsx b/plugins/user-settings/src/components/UserSettingsTab/UserSettingsTab.tsx index 171c54ecf3..ebddac00c1 100644 --- a/plugins/user-settings/src/components/UserSettingsTab/UserSettingsTab.tsx +++ b/plugins/user-settings/src/components/UserSettingsTab/UserSettingsTab.tsx @@ -15,11 +15,15 @@ */ import React, { PropsWithChildren } from 'react'; import { attachComponentData } from '@backstage/core-plugin-api'; +import { + LAYOUT_ROUTE_DATA_KEY, + SettingsLayout, +} from '../SettingsLayout/SettingsLayout'; -/** @public */ -export const USER_SETTINGS_TAB_KEY = 'user-settings.tab'; +/** @public @deprecated Use SettingsLayout.Route approach instead */ +export const USER_SETTINGS_TAB_KEY = LAYOUT_ROUTE_DATA_KEY; -/** @public */ +/** @public @deprecated Use SettingsLayoutRouteProps instead */ export type UserSettingsTabProps = PropsWithChildren<{ /** * The path to the tab in the settings route @@ -34,9 +38,12 @@ export type UserSettingsTabProps = PropsWithChildren<{ * Renders a tab inside the settings page * @param props - Component props * @public + * @deprecated Use SettingsLayout.Route instead */ -export const UserSettingsTab = (props: UserSettingsTabProps) => { - return <>{props.children}; -}; +export const UserSettingsTab = (props: UserSettingsTabProps) => ( + + <>props.children + +); attachComponentData(UserSettingsTab, USER_SETTINGS_TAB_KEY, 'UserSettingsTab'); diff --git a/plugins/user-settings/src/components/index.ts b/plugins/user-settings/src/components/index.ts index 8dc76ee7f1..8eb43a7a10 100644 --- a/plugins/user-settings/src/components/index.ts +++ b/plugins/user-settings/src/components/index.ts @@ -21,3 +21,4 @@ export * from './General'; export * from './FeatureFlags'; export { useUserProfile } from './useUserProfileInfo'; export * from './UserSettingsTab'; +export * from './SettingsLayout'; diff --git a/plugins/vault-backend/CHANGELOG.md b/plugins/vault-backend/CHANGELOG.md index 6f3270dba1..9aed902366 100644 --- a/plugins/vault-backend/CHANGELOG.md +++ b/plugins/vault-backend/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-vault-backend +## 0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.17.0-next.1 + - @backstage/backend-tasks@0.4.0-next.1 + - @backstage/backend-test-utils@0.1.31-next.1 + - @backstage/config@1.0.5-next.1 + - @backstage/errors@1.1.4-next.1 + +## 0.2.5-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- 7a3d2688ed: Use `express-promise-router` to catch errors properly. + Add `403` error as a known one. It will now return a `NotAllowed` error. +- Updated dependencies + - @backstage/backend-common@0.16.1-next.0 + - @backstage/backend-test-utils@0.1.31-next.0 + - @backstage/backend-tasks@0.3.8-next.0 + - @backstage/config@1.0.5-next.0 + - @backstage/errors@1.1.4-next.0 + +## 0.2.4 + +### Patch Changes + +- 687237da4c: Added `errorHandler()` middleware to `router` to prevent crashes caused by fatal errors in plugin backend +- Updated dependencies + - @backstage/backend-common@0.16.0 + - @backstage/backend-tasks@0.3.7 + - @backstage/backend-test-utils@0.1.30 + - @backstage/config@1.0.4 + - @backstage/errors@1.1.3 + +## 0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.16.0-next.1 + - @backstage/backend-tasks@0.3.7-next.1 + - @backstage/backend-test-utils@0.1.30-next.1 + - @backstage/config@1.0.4-next.0 + - @backstage/errors@1.1.3-next.0 + ## 0.2.4-next.0 ### Patch Changes diff --git a/plugins/vault-backend/README.md b/plugins/vault-backend/README.md index d7d227607d..c1b49f623a 100644 --- a/plugins/vault-backend/README.md +++ b/plugins/vault-backend/README.md @@ -63,7 +63,8 @@ To get started, first you need a running instance of Vault. You can follow [this ```yaml vault: - baseUrl: http://your-vault-url + baseUrl: http://your-internal-vault-url.svc + publicUrl: https://your-vault-url.example.com token: secretEngine: 'customSecretEngine' # Optional. By default it uses 'secrets' kvVersion: # Optional. The K/V version that your instance is using. The available options are '1' or '2' diff --git a/plugins/vault-backend/config.d.ts b/plugins/vault-backend/config.d.ts index c7aac4db9d..a30999b0cb 100644 --- a/plugins/vault-backend/config.d.ts +++ b/plugins/vault-backend/config.d.ts @@ -23,6 +23,12 @@ export interface Config { */ baseUrl: string; + /** + * The publicUrl for your Vault instance (Optional). + * @visibility frontend + */ + publicUrl?: string; + /** * The token used by Backstage to access Vault. * @visibility secret diff --git a/plugins/vault-backend/package.json b/plugins/vault-backend/package.json index 2cc170337a..65f978df73 100644 --- a/plugins/vault-backend/package.json +++ b/plugins/vault-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-vault-backend", "description": "A Backstage backend plugin that integrates towards Vault", - "version": "0.2.4-next.0", + "version": "0.2.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -53,7 +53,7 @@ "@backstage/cli": "workspace:^", "@types/compression": "^1.7.2", "@types/supertest": "^2.0.8", - "msw": "^0.47.0", + "msw": "^0.49.0", "supertest": "^6.1.6" }, "files": [ diff --git a/plugins/vault-backend/src/config/config.test.ts b/plugins/vault-backend/src/config/config.test.ts index 7cfdd8f468..a061d0f784 100644 --- a/plugins/vault-backend/src/config/config.test.ts +++ b/plugins/vault-backend/src/config/config.test.ts @@ -40,6 +40,7 @@ describe('GetVaultConfig', () => { const vaultConfig = getVaultConfig(config); expect(vaultConfig).toStrictEqual({ baseUrl: 'http://www.example.com', + publicUrl: undefined, token: '123', kvVersion: 2, secretEngine: 'secrets', @@ -59,6 +60,7 @@ describe('GetVaultConfig', () => { const vaultConfig = getVaultConfig(config); expect(vaultConfig).toStrictEqual({ baseUrl: 'http://www.example.com', + publicUrl: undefined, token: '123', kvVersion: 1, secretEngine: 'test', diff --git a/plugins/vault-backend/src/config/config.ts b/plugins/vault-backend/src/config/config.ts index 4920c73fed..159fee0447 100644 --- a/plugins/vault-backend/src/config/config.ts +++ b/plugins/vault-backend/src/config/config.ts @@ -27,6 +27,11 @@ export interface VaultConfig { */ baseUrl: string; + /** + * The publicUrl for your Vault instance (Optional). + */ + publicUrl?: string; + /** * The token used by Backstage to access Vault. */ @@ -53,6 +58,7 @@ export interface VaultConfig { export function getVaultConfig(config: Config): VaultConfig { return { baseUrl: config.getString('vault.baseUrl'), + publicUrl: config.getOptionalString('vault.publicUrl'), token: config.getString('vault.token'), kvVersion: config.getOptionalNumber('vault.kvVersion') ?? 2, secretEngine: config.getOptionalString('vault.secretEngine') ?? 'secrets', diff --git a/plugins/vault-backend/src/service/VaultBuilder.ts b/plugins/vault-backend/src/service/VaultBuilder.ts index 67260f45ca..788b90ff01 100644 --- a/plugins/vault-backend/src/service/VaultBuilder.ts +++ b/plugins/vault-backend/src/service/VaultBuilder.ts @@ -17,9 +17,11 @@ import { Config } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { Logger } from 'winston'; -import express, { Router } from 'express'; +import express from 'express'; +import Router from 'express-promise-router'; import { VaultClient } from './vaultApi'; import { TaskRunner, PluginTaskScheduler } from '@backstage/backend-tasks'; +import { errorHandler } from '@backstage/backend-common'; /** * Environment values needed by the VaultBuilder @@ -145,6 +147,7 @@ export class VaultBuilder { res.json({ items: secrets }); }); + router.use(errorHandler()); return router; } } diff --git a/plugins/vault-backend/src/service/vaultApi.ts b/plugins/vault-backend/src/service/vaultApi.ts index 785352fea8..7510ff218e 100644 --- a/plugins/vault-backend/src/service/vaultApi.ts +++ b/plugins/vault-backend/src/service/vaultApi.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { NotFoundError } from '@backstage/errors'; +import { NotAllowedError, NotFoundError } from '@backstage/errors'; import fetch from 'node-fetch'; import plimit from 'p-limit'; import { getVaultConfig, VaultConfig } from '../config'; @@ -103,6 +103,8 @@ export class VaultClient implements VaultApi { return (await response.json()) as T; } else if (response.status === 404) { throw new NotFoundError(`No secrets found in path '${path}'`); + } else if (response.status === 403) { + throw new NotAllowedError(response.statusText); } throw new Error( `Unexpected error while fetching secrets from path '${path}'`, @@ -133,11 +135,13 @@ export class VaultClient implements VaultApi { )), ); } else { + const vaultUrl = + this.vaultConfig.publicUrl || this.vaultConfig.baseUrl; secrets.push({ name: secret, path: secretPath, - editUrl: `${this.vaultConfig.baseUrl}/ui/vault/secrets/${this.vaultConfig.secretEngine}/edit/${secretPath}/${secret}`, - showUrl: `${this.vaultConfig.baseUrl}/ui/vault/secrets/${this.vaultConfig.secretEngine}/show/${secretPath}/${secret}`, + editUrl: `${vaultUrl}/ui/vault/secrets/${this.vaultConfig.secretEngine}/edit/${secretPath}/${secret}`, + showUrl: `${vaultUrl}/ui/vault/secrets/${this.vaultConfig.secretEngine}/show/${secretPath}/${secret}`, }); } }), diff --git a/plugins/vault/CHANGELOG.md b/plugins/vault/CHANGELOG.md index ce0f6c41cf..4fa9f69ca1 100644 --- a/plugins/vault/CHANGELOG.md +++ b/plugins/vault/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-vault +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/plugin-catalog-react@1.2.2-next.1 + - @backstage/catalog-model@1.1.4-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.1.6-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/plugin-catalog-react@1.2.2-next.0 + - @backstage/catalog-model@1.1.4-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.2.1 + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/catalog-model@1.1.3 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/catalog-model@1.1.3-next.0 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.2.1-next.1 + ## 0.1.5-next.0 ### Patch Changes diff --git a/plugins/vault/package.json b/plugins/vault/package.json index dc5e7b0c13..1b394670b5 100644 --- a/plugins/vault/package.json +++ b/plugins/vault/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-vault", "description": "A Backstage plugin that integrates towards Vault", - "version": "0.1.5-next.0", + "version": "0.1.6-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -57,7 +57,7 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "*", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index 8c68404127..dca149a889 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-xcmetrics +## 0.2.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.1-next.1 + - @backstage/core-plugin-api@1.1.1-next.1 + - @backstage/errors@1.1.4-next.1 + - @backstage/theme@0.2.16 + +## 0.2.32-next.0 + +### Patch Changes + +- 3280711113: Updated dependency `msw` to `^0.49.0`. +- Updated dependencies + - @backstage/core-components@0.12.1-next.0 + - @backstage/core-plugin-api@1.1.1-next.0 + - @backstage/errors@1.1.4-next.0 + - @backstage/theme@0.2.16 + +## 0.2.31 + +### Patch Changes + +- 7573b65232: Internal refactor of imports to avoid circular dependencies +- 43afded227: Updated recharts to v2.0.0 and fixed typing issues +- dcf9e728de: Removed an unused and hidden build details route. +- Updated dependencies + - @backstage/core-components@0.12.0 + - @backstage/core-plugin-api@1.1.0 + - @backstage/errors@1.1.3 + - @backstage/theme@0.2.16 + +## 0.2.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.0-next.1 + - @backstage/core-plugin-api@1.1.0-next.0 + - @backstage/errors@1.1.3-next.0 + - @backstage/theme@0.2.16 + ## 0.2.31-next.0 ### Patch Changes diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 6946095a01..f842bbf553 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-xcmetrics", "description": "A Backstage plugin that shows XCode build metrics for your components", - "version": "0.2.31-next.0", + "version": "0.2.32-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -49,7 +49,7 @@ "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", "cross-fetch": "^3.1.5", - "msw": "^0.47.0" + "msw": "^0.49.0" }, "files": [ "dist" diff --git a/scripts/check-if-release.js b/scripts/check-if-release.js index b61b0e2eb7..e7dabe14a4 100755 --- a/scripts/check-if-release.js +++ b/scripts/check-if-release.js @@ -28,6 +28,7 @@ const { execFile: execFileCb } = require('child_process'); const { resolve: resolvePath } = require('path'); const { promises: fs } = require('fs'); const { promisify } = require('util'); +const { EOL } = require('os'); const parentRef = process.env.COMMIT_SHA_BEFORE || 'HEAD^'; @@ -53,6 +54,10 @@ async function runPlain(cmd, ...args) { async function main() { process.cwd(resolvePath(__dirname, '..')); + if (!process.env.GITHUB_OUTPUT) { + throw new Error('GITHUB_OUTPUT environment variable not set'); + } + const diff = await runPlain( 'git', 'diff', @@ -103,7 +108,7 @@ async function main() { if (newVersions.length === 0) { console.log('No package version bumps detected, no release needed'); - console.log(`::set-output name=needs_release::false`); + await fs.appendFile(process.env.GITHUB_OUTPUT, `needs_release=false${EOL}`); return; } @@ -114,7 +119,7 @@ async function main() { ` ${name.padEnd(maxLength, ' ')} ${oldVersion} to ${newVersion}`, ); } - console.log(`::set-output name=needs_release::true`); + await fs.appendFile(process.env.GITHUB_OUTPUT, `needs_release=true${EOL}`); } main().catch(error => { diff --git a/scripts/create-release-tag.js b/scripts/create-release-tag.js index a2706c578f..28417024fd 100755 --- a/scripts/create-release-tag.js +++ b/scripts/create-release-tag.js @@ -19,6 +19,7 @@ const { Octokit } = require('@octokit/rest'); const path = require('path'); const fs = require('fs-extra'); +const { EOL } = require('os'); const baseOptions = { owner: 'backstage', @@ -64,6 +65,9 @@ async function main() { if (!process.env.GITHUB_TOKEN) { throw new Error('GITHUB_TOKEN is not set'); } + if (!process.env.GITHUB_OUTPUT) { + throw new Error('GITHUB_OUTPUT environment variable not set'); + } const commitSha = process.env.GITHUB_SHA; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); @@ -74,8 +78,11 @@ async function main() { console.log(`Creating release tag ${tagName} at ${commitSha}`); await createGitTag(octokit, commitSha, tagName); - console.log(`::set-output name=tag_name::${tagName}`); - console.log(`::set-output name=version::${releaseVersion}`); + await fs.appendFile(process.env.GITHUB_OUTPUT, `tag_name=${tagName}${EOL}`); + await fs.appendFile( + process.env.GITHUB_OUTPUT, + `version=${releaseVersion}${EOL}`, + ); } main().catch(error => { diff --git a/scripts/generate-merge-message.js b/scripts/generate-merge-message.js new file mode 100755 index 0000000000..f691c92c79 --- /dev/null +++ b/scripts/generate-merge-message.js @@ -0,0 +1,122 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const { execFile: execFileCb } = require('child_process'); +const { promisify } = require('util'); +const { resolve: resolvePath } = require('path'); + +const execFile = promisify(execFileCb); + +async function hasNewChangesets(baseRef, headRef) { + if (!baseRef) { + throw new Error('baseRef is required'); + } + if (!headRef) { + throw new Error('headRef is required'); + } + + const { stdout } = await execFile('git', [ + 'diff', + '--compact-summary', + baseRef, + headRef, + '--', + '.changeset/*.md', + ':(exclude).changeset/create-app-*.md', + ]); + return stdout.includes('(new)'); +} + +function getReleaseOfMonth(year, month) { + const base = new Date(Date.UTC(year, month)); + const wednesdayOffset = + base.getUTCDay() > 3 ? 10 - base.getUTCDay() : 3 - base.getUTCDay(); + const thirdWednesdayOffset = wednesdayOffset + 7 * 2; + const releaseOffset = thirdWednesdayOffset - 1; + const releaseDay = new Date( + Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), releaseOffset + 1), + ); + return releaseDay; +} + +function getReleaseSchedule() { + const firstReleaseYear = 2022; + const firstReleaseMonth = 2; + + return Array(100) + .fill(0) + .map((_, i) => { + const date = getReleaseOfMonth(firstReleaseYear, firstReleaseMonth + i); + return { version: `1.${i}.0`, date }; + }); +} + +function getCurrentRelease() { + const { version: releaseVersion } = require(resolvePath('package.json')); + + const match = releaseVersion.match(/^(\d+\.\d+\.\d+)/); + if (!match) { + throw new Error(`Failed to parse release version, '${releaseVersion}'`); + } + const [versionStr] = match; + if (versionStr === releaseVersion) { + return releaseVersion; + } + const [major, minor] = versionStr.split('.').map(Number); + return `${major}.${minor - 1}.0`; +} + +function findNextRelease(currentRelease, releaseSchedule) { + const currentIndex = releaseSchedule.findIndex( + r => r.version === currentRelease, + ); + if (currentIndex === -1) { + throw new Error( + `Failed to find current release '${currentRelease}' in release schedule`, + ); + } + + return releaseSchedule[currentIndex + 1]; +} + +async function main() { + const [diffBaseRefRef = 'origin/master', diffHeadRef = 'HEAD'] = + process.argv.slice(2); + const needsMessage = await hasNewChangesets(diffBaseRefRef, diffHeadRef); + if (!needsMessage) { + return; + } + + const currentRelease = getCurrentRelease(); + const releaseSchedule = getReleaseSchedule(); + const nextRelease = findNextRelease(currentRelease, releaseSchedule); + + const scheduledDate = nextRelease.date + .toUTCString() + .replace(/\s*\d+:\d+:\d+.*/, ''); + process.stdout.write( + [ + 'Thank you for contributing to Backstage! The changes in this pull request will be part', + `of the \`${nextRelease.version}\` release, scheduled for ${scheduledDate}.`, + ].join(' '), + ); +} + +main().catch(error => { + console.error(error.stack); + process.exit(1); +}); diff --git a/scripts/isolated-release.js b/scripts/isolated-release.js deleted file mode 100755 index 4df900450e..0000000000 --- a/scripts/isolated-release.js +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env node -/* - * 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. - */ - -const path = require('path'); -const childProcess = require('child_process'); -const { getPackages } = require('@manypkg/get-packages'); - -// Prepare a release of the provided packages, e.g. @backstage/core -async function main(args) { - if (args.includes('--help') || args.length === 0) { - const arg0 = path.relative(process.cwd(), process.argv[1]); - console.log(`Usage: ${process.argv0} ${arg0} ...`); - process.exit(1); - } - - const { packages } = await getPackages(__dirname); - const ignoreArgs = packages - .filter(p => !args.includes(p.packageJson.name)) - .flatMap(p => ['--ignore', p.packageJson.name]); - - const { status } = childProcess.spawnSync( - 'yarn', - ['changeset', 'version', ...ignoreArgs], - { - stdio: 'inherit', - }, - ); - if (status !== 0) { - return; - } - - childProcess.spawnSync( - 'yarn', - ['prettier', '--write', '{packages,plugins}/*/{package.json,CHANGELOG.md}'], - { - stdio: 'inherit', - }, - ); -} - -main(process.argv.slice(2)).catch(error => { - console.error(error.stack); - process.exit(1); -}); diff --git a/scripts/patch-release-for-pr.js b/scripts/patch-release-for-pr.js new file mode 100755 index 0000000000..be417e2911 --- /dev/null +++ b/scripts/patch-release-for-pr.js @@ -0,0 +1,170 @@ +#!/usr/bin/env node +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs-extra'); +const path = require('path'); +const semver = require('semver'); +const { Octokit } = require('@octokit/rest'); +const { execFile: execFileCb } = require('child_process'); +const { promisify } = require('util'); + +const execFile = promisify(execFileCb); + +const owner = 'backstage'; +const repo = 'backstage'; +const rootDir = path.resolve(__dirname, '..'); + +const octokit = new Octokit({ + auth: process.env.GITHUB_TOKEN, +}); + +async function run(command, ...args) { + const { stdout, stderr } = await execFile(command, args, { + cwd: rootDir, + }); + + if (stderr) { + console.error(stderr); + } + + return stdout.trim(); +} + +/** + * Finds the current stable release version of the repo, looking at + * the current commit and backwards, finding the first commit were a + * stable version is present. + */ +async function findCurrentReleaseVersion() { + const rootPkgPath = path.resolve(rootDir, 'package.json'); + const pkg = await fs.readJson(rootPkgPath); + + if (!semver.prerelease(pkg.version)) { + return pkg.version; + } + + const { stdout: revListStr } = await execFile('git', [ + 'rev-list', + 'HEAD', + '--', + 'package.json', + ]); + const revList = revListStr.trim().split(/\r?\n/); + + for (const rev of revList) { + const { stdout: pkgJsonStr } = await execFile('git', [ + 'show', + `${rev}:package.json`, + ]); + if (pkgJsonStr) { + const pkgJson = JSON.parse(pkgJsonStr); + if (!semver.prerelease(pkgJson.version)) { + return pkgJson.version; + } + } + } + + throw new Error('No stable release found'); +} + +async function main(prNumberStr) { + const prNumber = parseInt(prNumberStr, 10); + if (!Number.isInteger(prNumber)) { + throw new Error('Must provide a PR number as the first argument'); + } + console.log(`PR number: ${prNumber}`); + + if (await run('git', 'status', '--porcelain')) { + throw new Error('Cannot run with a dirty working tree'); + } + + const release = await findCurrentReleaseVersion(); + console.log(`Patching release ${release}`); + + await run('git', 'fetch'); + + const patchBranch = `patch/v${release}`; + try { + await run('git', 'checkout', `origin/${patchBranch}`); + } catch { + await run('git', 'checkout', '-b', patchBranch, `v${release}`); + await run('git', 'push', 'origin', '-u', patchBranch); + } + + const { data } = await octokit.pulls.get({ + owner, + repo, + pull_number: prNumber, + }); + + const headSha = data.head.sha; + if (!headSha) { + throw new Error('head sha not available'); + } + const baseSha = data.base.sha; + if (!baseSha) { + throw new Error('base sha not available'); + } + const mergeBaseSha = await run('git', 'merge-base', headSha, baseSha); + + // Create new branch, apply changes from all commits on PR branch, commit, push + const branchName = `patch-release-pr-${prNumber}`; + await run('git', 'checkout', '-b', branchName); + + const logLines = await run( + 'git', + 'log', + `${mergeBaseSha}...${headSha}`, + '--reverse', + '--pretty=%H', + ); + for (const logSha of logLines.split(/\r?\n/)) { + await run('git', 'cherry-pick', '-n', logSha); + } + await run( + 'git', + 'commit', + '--signoff', + '--no-verify', + '-m', + `Patch from PR #${prNumber}`, + ); + + console.log('Running "yarn release" ...'); + await run('yarn', 'release'); + + await run('git', 'add', '.'); + await run( + 'git', + 'commit', + '--signoff', + '--no-verify', + '-m', + 'Generate Release', + ); + + await run('git', 'push', 'origin', '-u', branchName); + + console.log( + `https://github.com/backstage/backstage/compare/${patchBranch}...${branchName}?expand=1&body=This%20release%20fixes%20an%20issue%20where&title=Patch%20release%20of%20%23${prNumber}`, + ); +} + +main(process.argv.slice(2)).catch(error => { + console.error(error.stack || error); + process.exit(1); +}); diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index 962d54abb0..973f4fad58 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -209,7 +209,11 @@ async function applyPatchVersions(repo, patchVersions) { const deps = packageJson[depType]; for (const depName of Object.keys(deps ?? {})) { const currentRange = deps[depName]; - if (currentRange === '*' || currentRange === '') { + if ( + currentRange === '*' || + currentRange === '' || + currentRange.startsWith('workspace:') + ) { continue; } diff --git a/scripts/verify-lockfile-duplicates.js b/scripts/verify-lockfile-duplicates.js new file mode 100644 index 0000000000..590de0f93d --- /dev/null +++ b/scripts/verify-lockfile-duplicates.js @@ -0,0 +1,135 @@ +#!/usr/bin/env node +/* + * 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 import/no-extraneous-dependencies */ + +const { execFile: execFileCb } = require('child_process'); +const { resolve: resolvePath, dirname: dirnamePath } = require('path'); +const { promisify } = require('util'); + +const execFile = promisify(execFileCb); + +async function findLockFiles() { + const projectRoot = resolvePath(__dirname, '..'); + + let files = process.argv.slice(2).filter(arg => !arg.startsWith('--')); + + for (const argumentFile of files) { + if (!argumentFile.match(/(?:^|[\/\\])yarn.lock$/)) { + throw new Error(`Not a yarn.lock file path argument: "${argumentFile}"`); + } + } + + if (!files.length) { + // List all lock files that are in the root or in an immediate subdirectory + files = ['yarn.lock', 'cypress/yarn.lock', 'microsite/yarn.lock']; + } + + return files.map(file => ({ + fileRelativeToProjectRoot: file, + directoryRelativeToProjectRoot: dirnamePath(file), + directoryAbsolute: resolvePath(projectRoot, dirnamePath(file)), + })); +} + +async function main() { + const lockFiles = await findLockFiles(); + + let fix = false; + for (const arg of process.argv) { + if (arg.startsWith('--')) { + if (arg === '--fix') { + fix = true; + } else { + throw new Error(`Unknown argument ${arg}`); + } + } + } + + for (const lockFile of lockFiles) { + console.log('Checking lock file', lockFile.fileRelativeToProjectRoot); + + let stdout; + let stderr; + let failed; + + try { + const result = await execFile( + 'yarn', + ['dedupe', ...(fix ? [] : ['--check'])], + { + shell: true, + cwd: lockFile.directoryAbsolute, + }, + ); + stdout = result.stdout?.trim(); + stderr = result.stderr?.trim(); + failed = false; + } catch (error) { + stdout = error.stdout?.trim(); + stderr = error.stderr?.trim(); + failed = true; + } + + if (stdout) { + console.log(stdout); + } + + if (stderr) { + console.error(stderr); + } + + if (failed) { + if (!fix) { + const command = `yarn dedupe${ + lockFile.directoryRelativeToProjectRoot === '.' + ? '' + : ` --cwd ${lockFile.directoryRelativeToProjectRoot}` + }`; + const padding = ' '.repeat(Math.max(0, 85 - 6 - command.length)); + console.error(''); + console.error( + '*************************************************************************************', + ); + console.error( + '* You have duplicate versions of some packages in a yarn.lock file. *', + ); + console.error( + '* To solve this, run the following command from the project root and commit all *', + ); + console.log( + '* yarn.lock changes. *', + ); + console.log( + '* *', + ); + console.log(`* ${command}${padding} *`); + console.error( + '*************************************************************************************', + ); + console.error(''); + } + + process.exit(1); + } + } +} + +main().catch(error => { + console.error(error.stack); + process.exit(1); +}); diff --git a/storybook/yarn.lock b/storybook/yarn.lock index be9a98e42a..ac821fc16c 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -1528,18 +1528,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.2.0, @babel/types@npm:^7.4.4": - version: 7.18.10 - resolution: "@babel/types@npm:7.18.10" - dependencies: - "@babel/helper-string-parser": ^7.18.10 - "@babel/helper-validator-identifier": ^7.18.6 - to-fast-properties: ^2.0.0 - checksum: 11632c9b106e54021937a6498138014ebc9ad6c327a07b2af3ba8700773945aba4055fd136431cbe3a500d0f363cbf9c68eb4d6d38229897c5de9d06e14c85e8 - languageName: node - linkType: hard - -"@babel/types@npm:^7.8.3": +"@babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.2.0, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.18.13 resolution: "@babel/types@npm:7.18.13" dependencies: @@ -1812,17 +1801,17 @@ __metadata: linkType: hard "@storybook/addon-a11y@npm:^6.5.9": - version: 6.5.13 - resolution: "@storybook/addon-a11y@npm:6.5.13" + version: 6.5.14 + resolution: "@storybook/addon-a11y@npm:6.5.14" dependencies: - "@storybook/addons": 6.5.13 - "@storybook/api": 6.5.13 - "@storybook/channels": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/components": 6.5.13 - "@storybook/core-events": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/api": 6.5.14 + "@storybook/channels": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/components": 6.5.14 + "@storybook/core-events": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/theming": 6.5.13 + "@storybook/theming": 6.5.14 axe-core: ^4.2.0 core-js: ^3.8.2 global: ^4.4.0 @@ -1839,21 +1828,21 @@ __metadata: optional: true react-dom: optional: true - checksum: 6f1ba1f0d97d652a5346a33e051e1ff79aa07786532c6450c5c3dd677c2195e3ee8792cda756d4556e5fbaa2a72a0631843aec2880121b3439bad3e401c25359 + checksum: 2c35e693641b38e90989a656b8766cd2a86482e24164e62afd2cd1899d454b1a0c0b8f23df328c5350884d22615e9779dd617c71c642f4eada1df6345feba8e3 languageName: node linkType: hard "@storybook/addon-actions@npm:^6.5.9": - version: 6.5.13 - resolution: "@storybook/addon-actions@npm:6.5.13" + version: 6.5.14 + resolution: "@storybook/addon-actions@npm:6.5.14" dependencies: - "@storybook/addons": 6.5.13 - "@storybook/api": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/components": 6.5.13 - "@storybook/core-events": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/api": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/components": 6.5.14 + "@storybook/core-events": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/theming": 6.5.13 + "@storybook/theming": 6.5.14 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 global: ^4.4.0 @@ -1874,23 +1863,23 @@ __metadata: optional: true react-dom: optional: true - checksum: 2679174b1467281860cd6f11fac5ba505e44629eb11ee90713d1c954569cfb54d80aa822a0ed1be2f92f11ca62889fec3dca96f61b89cf238503e0cea4b1db9a + checksum: 536a5afb4b9d2fbe6b250657deb36d5ef4eba128101fbe69bf00f9c363859f61e2337a460d0a724607ad48f54713bcc40ed54253df3fee4bdb23baf93a58defd languageName: node linkType: hard "@storybook/addon-controls@npm:^6.5.9": - version: 6.5.13 - resolution: "@storybook/addon-controls@npm:6.5.13" + version: 6.5.14 + resolution: "@storybook/addon-controls@npm:6.5.14" dependencies: - "@storybook/addons": 6.5.13 - "@storybook/api": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/components": 6.5.13 - "@storybook/core-common": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/api": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/components": 6.5.14 + "@storybook/core-common": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/node-logger": 6.5.13 - "@storybook/store": 6.5.13 - "@storybook/theming": 6.5.13 + "@storybook/node-logger": 6.5.14 + "@storybook/store": 6.5.14 + "@storybook/theming": 6.5.14 core-js: ^3.8.2 lodash: ^4.17.21 ts-dedent: ^2.0.0 @@ -1902,19 +1891,19 @@ __metadata: optional: true react-dom: optional: true - checksum: a4f86332686b5681366ad1f1eebb50cb9939ce3424ade64c0043b94827df15a34da0190101c8dc9a2b4b9af67fafd9d3fb50a5b98ff7a24fed6a37a2f8f37f27 + checksum: cebb9b33f944e9492527f4fd2bdfe73aa0f2737d53a36d7ea086499adc568ba06958e69c75191d5b266063ece513d5f7a484ec1da8f411f326aa4b022cce7ea0 languageName: node linkType: hard "@storybook/addon-links@npm:^6.5.9": - version: 6.5.13 - resolution: "@storybook/addon-links@npm:6.5.13" + version: 6.5.14 + resolution: "@storybook/addon-links@npm:6.5.14" dependencies: - "@storybook/addons": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/core-events": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.13 + "@storybook/router": 6.5.14 "@types/qs": ^6.9.5 core-js: ^3.8.2 global: ^4.4.0 @@ -1930,24 +1919,24 @@ __metadata: optional: true react-dom: optional: true - checksum: 0bbe14652320c77dbcbcd0a6f45e4776de35475cca43b9825c36424e6ccc250fac4a172cdf01304debdc3c93d74ad2ea3bafb8a752e9a52a5e860f5bb9a397c2 + checksum: 7dc537aab36fd80ba704b8618c24aa0b9f59d6c28502dd43c01e7dae4194fbce37a95cb7797c4b52547d9662bf9619cd0901f946bddcec7580986df2e38da574 languageName: node linkType: hard "@storybook/addon-storysource@npm:^6.5.9": - version: 6.5.13 - resolution: "@storybook/addon-storysource@npm:6.5.13" + version: 6.5.14 + resolution: "@storybook/addon-storysource@npm:6.5.14" dependencies: - "@storybook/addons": 6.5.13 - "@storybook/api": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/components": 6.5.13 - "@storybook/router": 6.5.13 - "@storybook/source-loader": 6.5.13 - "@storybook/theming": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/api": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/components": 6.5.14 + "@storybook/router": 6.5.14 + "@storybook/source-loader": 6.5.14 + "@storybook/theming": 6.5.14 core-js: ^3.8.2 estraverse: ^5.2.0 - loader-utils: ^2.0.0 + loader-utils: ^2.0.4 prop-types: ^15.7.2 react-syntax-highlighter: ^15.4.5 regenerator-runtime: ^0.13.7 @@ -1959,7 +1948,7 @@ __metadata: optional: true react-dom: optional: true - checksum: 35ad7b940df64b036c36edc1d203da8016203c2430ae98a380b33fef4ffbfe040651db61f54ef5a9e6edec298e58e660ddd933eb67a1563ce1e39faedbc56cba + checksum: d9962b3f03245ab7dce405e7f1a24291d985aa1ecc594836b6322185bcde1c61a84397acfd46e85b2818f00c1de0f564bf7b6e1d84b960418cbe3dc37660d3f9 languageName: node linkType: hard @@ -1985,17 +1974,17 @@ __metadata: languageName: node linkType: hard -"@storybook/addons@npm:6.5.13, @storybook/addons@npm:^6.5.9": - version: 6.5.13 - resolution: "@storybook/addons@npm:6.5.13" +"@storybook/addons@npm:6.5.14, @storybook/addons@npm:^6.0.0, @storybook/addons@npm:^6.5.9": + version: 6.5.14 + resolution: "@storybook/addons@npm:6.5.14" dependencies: - "@storybook/api": 6.5.13 - "@storybook/channels": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/core-events": 6.5.13 + "@storybook/api": 6.5.14 + "@storybook/channels": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.13 - "@storybook/theming": 6.5.13 + "@storybook/router": 6.5.14 + "@storybook/theming": 6.5.14 "@types/webpack-env": ^1.16.0 core-js: ^3.8.2 global: ^4.4.0 @@ -2003,29 +1992,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 28589da00e8a26b44d4ed8a1938fe934187a85b187e2a0dcd3e6d114460ed5a07fbfe0a89a4d899739767b379015cbabadd47c5c266457922a8c4255e3d769a4 - languageName: node - linkType: hard - -"@storybook/addons@npm:^6.0.0": - version: 6.5.12 - resolution: "@storybook/addons@npm:6.5.12" - dependencies: - "@storybook/api": 6.5.12 - "@storybook/channels": 6.5.12 - "@storybook/client-logger": 6.5.12 - "@storybook/core-events": 6.5.12 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.12 - "@storybook/theming": 6.5.12 - "@types/webpack-env": ^1.16.0 - core-js: ^3.8.2 - global: ^4.4.0 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: c6242a80c7355544eb309603e77fdc3787d78ad983aba931f00812aeba75cc2cbd0e98c1ac0ce01441b58fabcdb671a9a799358f4bd6511cab289bc030d91f61 + checksum: 99d06641bab06a3cc2821f309589d062c0efd8707b451ae24017449034da408bfddce3beda1ccdedadf59669d7d13348bee127f6fd4fc057200c84ff43288312 languageName: node linkType: hard @@ -2057,17 +2024,17 @@ __metadata: languageName: node linkType: hard -"@storybook/api@npm:6.5.12, @storybook/api@npm:^6.0.0": - version: 6.5.12 - resolution: "@storybook/api@npm:6.5.12" +"@storybook/api@npm:6.5.14, @storybook/api@npm:^6.0.0": + version: 6.5.14 + resolution: "@storybook/api@npm:6.5.14" dependencies: - "@storybook/channels": 6.5.12 - "@storybook/client-logger": 6.5.12 - "@storybook/core-events": 6.5.12 + "@storybook/channels": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.12 + "@storybook/router": 6.5.14 "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.5.12 + "@storybook/theming": 6.5.14 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 global: ^4.4.0 @@ -2081,59 +2048,31 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 3982cea5aaf851ccc19ff97ef82b7590d47839f0ebee28399e3b9381578edc130b4e46fe36431c62fa281949b2e0d5da2b1feafab0c2d24f70c4097d800b2679 + checksum: 0d421c3211a49cb8910dea647b898edd60af79755108ed321626a8fc134713dd1b018c830f15c2fc6c863f0528b571c2e2b34bb79df3c2f43497f5ab36fa9bbf languageName: node linkType: hard -"@storybook/api@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/api@npm:6.5.13" - dependencies: - "@storybook/channels": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/core-events": 6.5.13 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/router": 6.5.13 - "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.5.13 - core-js: ^3.8.2 - fast-deep-equal: ^3.1.3 - global: ^4.4.0 - lodash: ^4.17.21 - memoizerific: ^1.11.3 - regenerator-runtime: ^0.13.7 - store2: ^2.12.0 - telejson: ^6.0.8 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: dd7c8db0cdea2a47ab835c02217f10f99c54bfbf6d826deadf0b160ece4c94b1cb2558cfbaff4e4244c5c776095028a164762bd8de19fcfe10ae318fe0a3fbb4 - languageName: node - linkType: hard - -"@storybook/builder-webpack4@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/builder-webpack4@npm:6.5.13" +"@storybook/builder-webpack4@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/builder-webpack4@npm:6.5.14" dependencies: "@babel/core": ^7.12.10 - "@storybook/addons": 6.5.13 - "@storybook/api": 6.5.13 - "@storybook/channel-postmessage": 6.5.13 - "@storybook/channels": 6.5.13 - "@storybook/client-api": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/components": 6.5.13 - "@storybook/core-common": 6.5.13 - "@storybook/core-events": 6.5.13 - "@storybook/node-logger": 6.5.13 - "@storybook/preview-web": 6.5.13 - "@storybook/router": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/api": 6.5.14 + "@storybook/channel-postmessage": 6.5.14 + "@storybook/channels": 6.5.14 + "@storybook/client-api": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/components": 6.5.14 + "@storybook/core-common": 6.5.14 + "@storybook/core-events": 6.5.14 + "@storybook/node-logger": 6.5.14 + "@storybook/preview-web": 6.5.14 + "@storybook/router": 6.5.14 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.13 - "@storybook/theming": 6.5.13 - "@storybook/ui": 6.5.13 + "@storybook/store": 6.5.14 + "@storybook/theming": 6.5.14 + "@storybook/ui": 6.5.14 "@types/node": ^14.0.10 || ^16.0.0 "@types/webpack": ^4.41.26 autoprefixer: ^9.8.6 @@ -2170,30 +2109,30 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: a95fea3951479d7724155a2ddbf2b04a8bfc0e7fddbf8415caed508b94ad71f7dc8d5d25464061ef26f3b4670f49f6ed40198b48b3f646d7af17d8daee5e89cb + checksum: 16749d2d03b82e43e6b0fea02ebd4b63ac961229e4203cb371f36f11a9cd0a484d9bef73ee443053e09594ba7a7f2311470ade0e5a246b6d9345d1c7cc7b83b0 languageName: node linkType: hard "@storybook/builder-webpack5@npm:^6.5.9": - version: 6.5.13 - resolution: "@storybook/builder-webpack5@npm:6.5.13" + version: 6.5.14 + resolution: "@storybook/builder-webpack5@npm:6.5.14" dependencies: "@babel/core": ^7.12.10 - "@storybook/addons": 6.5.13 - "@storybook/api": 6.5.13 - "@storybook/channel-postmessage": 6.5.13 - "@storybook/channels": 6.5.13 - "@storybook/client-api": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/components": 6.5.13 - "@storybook/core-common": 6.5.13 - "@storybook/core-events": 6.5.13 - "@storybook/node-logger": 6.5.13 - "@storybook/preview-web": 6.5.13 - "@storybook/router": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/api": 6.5.14 + "@storybook/channel-postmessage": 6.5.14 + "@storybook/channels": 6.5.14 + "@storybook/client-api": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/components": 6.5.14 + "@storybook/core-common": 6.5.14 + "@storybook/core-events": 6.5.14 + "@storybook/node-logger": 6.5.14 + "@storybook/preview-web": 6.5.14 + "@storybook/router": 6.5.14 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.13 - "@storybook/theming": 6.5.13 + "@storybook/store": 6.5.14 + "@storybook/theming": 6.5.14 "@types/node": ^14.0.10 || ^16.0.0 babel-loader: ^8.0.0 babel-plugin-named-exports-order: ^0.0.2 @@ -2222,35 +2161,35 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: f980ab5c832bac9584f80eb0229934bb58ceb4cf24f756043acea4cb397f52f7af49c1fd53ac897aefb5ffc9c808bcb8a0c4867afd2c2a21b7ba2f0a56ae0089 + checksum: 3aa60285baa51b3249b43b2fde92100ad24a5f383354b0e72a7915125f25386ace3e69a8718fb5baafc7cf8deca9e005c4ce0a9b356a1ca7016abe2254466115 languageName: node linkType: hard -"@storybook/channel-postmessage@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/channel-postmessage@npm:6.5.13" +"@storybook/channel-postmessage@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/channel-postmessage@npm:6.5.14" dependencies: - "@storybook/channels": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/core-events": 6.5.13 + "@storybook/channels": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 core-js: ^3.8.2 global: ^4.4.0 qs: ^6.10.0 telejson: ^6.0.8 - checksum: 8d6ccfff2aeafaae30b5fc1af856be8d06b3703b96841ecc0d70959e51542514901763e1a291e1d0278afe31b23cc5c0a5b351994f321e9bd03490be5b51e2d0 + checksum: 2ea7aed1e49808875788746db75d61d3180a0b883892e978e93326d61617c0e01b7816b9eefcad402579d9f6dd923a13b527bcfcda3f415c0a135987d6c2042d languageName: node linkType: hard -"@storybook/channel-websocket@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/channel-websocket@npm:6.5.13" +"@storybook/channel-websocket@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/channel-websocket@npm:6.5.14" dependencies: - "@storybook/channels": 6.5.13 - "@storybook/client-logger": 6.5.13 + "@storybook/channels": 6.5.14 + "@storybook/client-logger": 6.5.14 core-js: ^3.8.2 global: ^4.4.0 telejson: ^6.0.8 - checksum: 16e3b1a51a1af093f6c78ab7ca9c4c69ed05b45fd9bbdefb3050809e92064ccbf7a46f6800d21df2555e5d110d64735dd8d35155e54c3118fa7b4efe6b3b0457 + checksum: 76bd5734890836d6d7af8fd619015067db4680ae4336fdf86ee1c612e0060412c14cfcba22faee02534a63079af0864323370409730c7f3ee16af8fc3014e758 languageName: node linkType: hard @@ -2265,39 +2204,28 @@ __metadata: languageName: node linkType: hard -"@storybook/channels@npm:6.5.12": - version: 6.5.12 - resolution: "@storybook/channels@npm:6.5.12" +"@storybook/channels@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/channels@npm:6.5.14" dependencies: core-js: ^3.8.2 ts-dedent: ^2.0.0 util-deprecate: ^1.0.2 - checksum: e6b240a6c62a68a485bf8f4db536df0504cfcbe9685654e5a5712b833917b9a620e91994bf2283a420e413511c967e92ead522c98ad7e6c0e88b3830ddfd4e30 + checksum: ff1ee3fea3c7b8591280ba7eabe13c999fc3e12a483ff2c0467cc9cca027662cbbc4676438da567865919157521df8a9a50bd20b35daed6896f39a3a7251a1e5 languageName: node linkType: hard -"@storybook/channels@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/channels@npm:6.5.13" +"@storybook/client-api@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/client-api@npm:6.5.14" dependencies: - core-js: ^3.8.2 - ts-dedent: ^2.0.0 - util-deprecate: ^1.0.2 - checksum: 5b8881a2799a4c5ceafea40bc2c8bad1a31649036341eec8da5a77acf79a9d610afeaa5b4ed5d06022ed3c74cb9562dcfc5046d62fd8d27cd65bcba09aa5e903 - languageName: node - linkType: hard - -"@storybook/client-api@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/client-api@npm:6.5.13" - dependencies: - "@storybook/addons": 6.5.13 - "@storybook/channel-postmessage": 6.5.13 - "@storybook/channels": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/core-events": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/channel-postmessage": 6.5.14 + "@storybook/channels": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/store": 6.5.13 + "@storybook/store": 6.5.14 "@types/qs": ^6.9.5 "@types/webpack-env": ^1.16.0 core-js: ^3.8.2 @@ -2314,11 +2242,11 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: b0af25786b9144a55ebaa7754dd1b3701f5f8796770eaf59e7bc6d21ada12911fcbe4bf0da037d01bdda2c46138f265a948befe1f3de356fdc0ae3af80973388 + checksum: 46fe2bec19610a7ab59b41f25653261cb65b485a829a94f99f9aa01f1f5bbc13677979abfe94eb025f0d9c1138b284ddbeb6ce8a0ae392ce4995a813770be83b languageName: node linkType: hard -"@storybook/client-logger@npm:6.5.10, @storybook/client-logger@npm:^6.4.0": +"@storybook/client-logger@npm:6.5.10": version: 6.5.10 resolution: "@storybook/client-logger@npm:6.5.10" dependencies: @@ -2328,33 +2256,23 @@ __metadata: languageName: node linkType: hard -"@storybook/client-logger@npm:6.5.12": - version: 6.5.12 - resolution: "@storybook/client-logger@npm:6.5.12" +"@storybook/client-logger@npm:6.5.14, @storybook/client-logger@npm:^6.4.0": + version: 6.5.14 + resolution: "@storybook/client-logger@npm:6.5.14" dependencies: core-js: ^3.8.2 global: ^4.4.0 - checksum: bd11bc25115f9b4a965e378d7dac28f9152038173ab5debb1e116a7aba69c814752d2c8aa4092dd1fc3f60cd99d4896c9e74d5e6f3c85768e7633adaf5bd2bf2 + checksum: 29cc0b58db7a8dc90484320c86b386975580c0e534791b29f6a8c00ce5b156f2bff9513994202f9f9ef99787e8d793988048ae88d2780ba151c6782f3bbf97ff languageName: node linkType: hard -"@storybook/client-logger@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/client-logger@npm:6.5.13" +"@storybook/components@npm:6.5.14, @storybook/components@npm:^6.0.0": + version: 6.5.14 + resolution: "@storybook/components@npm:6.5.14" dependencies: - core-js: ^3.8.2 - global: ^4.4.0 - checksum: 0252d9364a0b2a8faae588fdb29aaf458f660904c330ec7af790f63a668710926ece8f087f58f9b1bebb052e2fe517b8b74867e7500567499cc710ab71ccbbab - languageName: node - linkType: hard - -"@storybook/components@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/components@npm:6.5.13" - dependencies: - "@storybook/client-logger": 6.5.13 + "@storybook/client-logger": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/theming": 6.5.13 + "@storybook/theming": 6.5.14 core-js: ^3.8.2 memoizerific: ^1.11.3 qs: ^6.10.0 @@ -2363,43 +2281,24 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 5d01c0f445f6574ccadcfa79afd99c078bd1f81d65e59186361100dc57bd73ccbb877e5a8bbc49dd6551bce1b32fbe6f135c2bea15c0126a83faf222cfaed878 + checksum: 73d0ff418f2c5b1fc9f421f089ebee2342ef0684905e4959397a8bc6f67f32ff97f4ec3587847980cb7e4fc8e40278d8ecea11a8d267fea8d94ad3d8265a0c21 languageName: node linkType: hard -"@storybook/components@npm:^6.0.0": - version: 6.5.12 - resolution: "@storybook/components@npm:6.5.12" +"@storybook/core-client@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/core-client@npm:6.5.14" dependencies: - "@storybook/client-logger": 6.5.12 + "@storybook/addons": 6.5.14 + "@storybook/channel-postmessage": 6.5.14 + "@storybook/channel-websocket": 6.5.14 + "@storybook/client-api": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/theming": 6.5.12 - core-js: ^3.8.2 - memoizerific: ^1.11.3 - qs: ^6.10.0 - regenerator-runtime: ^0.13.7 - util-deprecate: ^1.0.2 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: fa469ae615d9146df7e23f01b85731d27e6400e2d94035db172deb1f61903d86c121d858558dd12307ecc6344d21b496db020731e73eff6ace3f82672b953a93 - languageName: node - linkType: hard - -"@storybook/core-client@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/core-client@npm:6.5.13" - dependencies: - "@storybook/addons": 6.5.13 - "@storybook/channel-postmessage": 6.5.13 - "@storybook/channel-websocket": 6.5.13 - "@storybook/client-api": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/core-events": 6.5.13 - "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/preview-web": 6.5.13 - "@storybook/store": 6.5.13 - "@storybook/ui": 6.5.13 + "@storybook/preview-web": 6.5.14 + "@storybook/store": 6.5.14 + "@storybook/ui": 6.5.14 airbnb-js-shims: ^2.2.1 ansi-to-html: ^0.6.11 core-js: ^3.8.2 @@ -2417,13 +2316,13 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: c4350b1b579f0781a239fdede79f1d0975e297ecb61ba4096834d62bd553420615231dc9146c446d0178088e83863fd9dc720fbb4485b5779fac7d99ce3eeb9e + checksum: ff867021ab6c22408952b809f3c22e58474390062fb5417aecc0ea6dcf2eb0097a616c5b1377e9af6c16045ce0cd5f3a11b32072630085592998dbb6a7ee573d languageName: node linkType: hard -"@storybook/core-common@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/core-common@npm:6.5.13" +"@storybook/core-common@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/core-common@npm:6.5.14" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-proposal-class-properties": ^7.12.1 @@ -2447,7 +2346,7 @@ __metadata: "@babel/preset-react": ^7.12.10 "@babel/preset-typescript": ^7.12.7 "@babel/register": ^7.12.1 - "@storybook/node-logger": 6.5.13 + "@storybook/node-logger": 6.5.14 "@storybook/semver": ^7.3.2 "@types/node": ^14.0.10 || ^16.0.0 "@types/pretty-hrtime": ^1.0.0 @@ -2481,7 +2380,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 369fbe41e9ac657410a8e7fb4668be0e77c50b84c29b352397cc26b72d79397a7e84dcbf7a94f2d02d819d395a66e30a3915de40e85936d7b7dc50bb426aeabb + checksum: a1cd6e97524aa8173bc8a0af867cbf407f0542b8577a5be79a82154efb239d9362d05fae3bad0aac41e17d33d950cb69290042be46bbc841a8d12b71f6db1c82 languageName: node linkType: hard @@ -2494,40 +2393,31 @@ __metadata: languageName: node linkType: hard -"@storybook/core-events@npm:6.5.12, @storybook/core-events@npm:^6.0.0": - version: 6.5.12 - resolution: "@storybook/core-events@npm:6.5.12" +"@storybook/core-events@npm:6.5.14, @storybook/core-events@npm:^6.0.0": + version: 6.5.14 + resolution: "@storybook/core-events@npm:6.5.14" dependencies: core-js: ^3.8.2 - checksum: 82a4b9cb2a8599f3916db84b08b4cfbde8f56cb96a7afe641b3f144676fc7dc5a705e65f8844430b36ee6e6e14d6b2cb741622a8b39411276682219df5e04271 + checksum: 6787925c520a6ee5aee748d4b7e2ec599c5ee16a87dbb62a94eeec88003ef42683d8e7ac8b98b49ea2a33205e0648805410c4759d16a997ba2f4215f6c8784ce languageName: node linkType: hard -"@storybook/core-events@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/core-events@npm:6.5.13" - dependencies: - core-js: ^3.8.2 - checksum: 2afeaf5fd658a4e9eedb9ad458ba0bcc73ad4ae5ba0e9971434818258db01d9b48b604d4db396ebfc1e1571dace3f6659e9ed61ac35428a792a4e24bbc08b29c - languageName: node - linkType: hard - -"@storybook/core-server@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/core-server@npm:6.5.13" +"@storybook/core-server@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/core-server@npm:6.5.14" dependencies: "@discoveryjs/json-ext": ^0.5.3 - "@storybook/builder-webpack4": 6.5.13 - "@storybook/core-client": 6.5.13 - "@storybook/core-common": 6.5.13 - "@storybook/core-events": 6.5.13 + "@storybook/builder-webpack4": 6.5.14 + "@storybook/core-client": 6.5.14 + "@storybook/core-common": 6.5.14 + "@storybook/core-events": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/csf-tools": 6.5.13 - "@storybook/manager-webpack4": 6.5.13 - "@storybook/node-logger": 6.5.13 + "@storybook/csf-tools": 6.5.14 + "@storybook/manager-webpack4": 6.5.14 + "@storybook/node-logger": 6.5.14 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.13 - "@storybook/telemetry": 6.5.13 + "@storybook/store": 6.5.14 + "@storybook/telemetry": 6.5.14 "@types/node": ^14.0.10 || ^16.0.0 "@types/node-fetch": ^2.5.7 "@types/pretty-hrtime": ^1.0.0 @@ -2571,16 +2461,16 @@ __metadata: optional: true typescript: optional: true - checksum: 142b13ef4fef21a68c8255f35f42ca5c3b9f636b51986f61dc6ae95485788d0f581552604b8a4a796e7cd2ad9548b87317ac9647ea44be58f64b91c440bb71ea + checksum: ca61b13ff002f73f76f6d359fc7955f061806d13bb0f158d6ff299696901097329d082b4fdd7fac06dda93fd318bcdbba9f6a08390763d18851c8aa70e581c36 languageName: node linkType: hard -"@storybook/core@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/core@npm:6.5.13" +"@storybook/core@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/core@npm:6.5.14" dependencies: - "@storybook/core-client": 6.5.13 - "@storybook/core-server": 6.5.13 + "@storybook/core-client": 6.5.14 + "@storybook/core-server": 6.5.14 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -2592,13 +2482,13 @@ __metadata: optional: true typescript: optional: true - checksum: e0dbe5d8d52f2a12ab63db965d5d15ee671029a03b7204756954e2bc3253b560c8e430aaab5559ccbddcfd3e97d2bc1c0c58ed370aa593912724402aa86996b8 + checksum: 16a2b8c589cbb8632ef6f1ba941abd7a422bca6fa54d4ef1dc5a60145bbea8dded10c7fa1fd4b8284e2e86867e779c26d4eebe69e2156123c49cfdf66c14718a languageName: node linkType: hard -"@storybook/csf-tools@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/csf-tools@npm:6.5.13" +"@storybook/csf-tools@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/csf-tools@npm:6.5.14" dependencies: "@babel/core": ^7.12.10 "@babel/generator": ^7.12.11 @@ -2619,7 +2509,7 @@ __metadata: peerDependenciesMeta: "@storybook/mdx2-csf": optional: true - checksum: 2b8a5bed04ea89084334742e1095c4565b0b7367b5126e3a9b6648224b59c2136a9d57cbb9067264fc3951e9db58df40b23b975170180d171cce35dfabf2a090 + checksum: 12e6bb4d9daa1f3ffd6a240edd9680c4088a20f2eba6dd91102fd2900188dcfcec60fc7e7b021aa2ae2232e15e0839aaf490d1c2ab2c9baa74a9dfea74ec713f languageName: node linkType: hard @@ -2632,18 +2522,18 @@ __metadata: languageName: node linkType: hard -"@storybook/docs-tools@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/docs-tools@npm:6.5.13" +"@storybook/docs-tools@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/docs-tools@npm:6.5.14" dependencies: "@babel/core": ^7.12.10 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/store": 6.5.13 + "@storybook/store": 6.5.14 core-js: ^3.8.2 doctrine: ^3.0.0 lodash: ^4.17.21 regenerator-runtime: ^0.13.7 - checksum: d3ad4674922025aaf6e4e2b7c2ac6f4eaec8f5692dc9a792a15d2d5e38dcd2e1c138daffb31a3da04da1e04c645b3cd8d921890f9ba318bfee7b6a12f263b48a + checksum: 1a69dae1f9ae4196a3517571d898097a69431cd6d92f490af3eb3e5504fe0bbde4cbdd82d1ff9648c3a1daa7eab10eaffd5d2ee485e634a6fa5701dec7875196 languageName: node linkType: hard @@ -2660,19 +2550,19 @@ __metadata: languageName: node linkType: hard -"@storybook/manager-webpack4@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/manager-webpack4@npm:6.5.13" +"@storybook/manager-webpack4@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/manager-webpack4@npm:6.5.14" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-transform-template-literals": ^7.12.1 "@babel/preset-react": ^7.12.10 - "@storybook/addons": 6.5.13 - "@storybook/core-client": 6.5.13 - "@storybook/core-common": 6.5.13 - "@storybook/node-logger": 6.5.13 - "@storybook/theming": 6.5.13 - "@storybook/ui": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/core-client": 6.5.14 + "@storybook/core-common": 6.5.14 + "@storybook/node-logger": 6.5.14 + "@storybook/theming": 6.5.14 + "@storybook/ui": 6.5.14 "@types/node": ^14.0.10 || ^16.0.0 "@types/webpack": ^4.41.26 babel-loader: ^8.0.0 @@ -2705,23 +2595,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 6645f30b6199d0badb2097aca16478e4d8bc88210b55e60c4da86962e8b0a3441128f1e696cddf8a535591920617a7abf5f1f9771dabc486479e76b8d4b20fbd + checksum: c8547b64f80c87dd8590a3e7b784b6f72952607514f9e42858f89efce22ea077404033b25674aecc3874e61264ce74c38220b6130aecbce5361d182b018f7fc7 languageName: node linkType: hard "@storybook/manager-webpack5@npm:^6.5.9": - version: 6.5.13 - resolution: "@storybook/manager-webpack5@npm:6.5.13" + version: 6.5.14 + resolution: "@storybook/manager-webpack5@npm:6.5.14" dependencies: "@babel/core": ^7.12.10 "@babel/plugin-transform-template-literals": ^7.12.1 "@babel/preset-react": ^7.12.10 - "@storybook/addons": 6.5.13 - "@storybook/core-client": 6.5.13 - "@storybook/core-common": 6.5.13 - "@storybook/node-logger": 6.5.13 - "@storybook/theming": 6.5.13 - "@storybook/ui": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/core-client": 6.5.14 + "@storybook/core-common": 6.5.14 + "@storybook/node-logger": 6.5.14 + "@storybook/theming": 6.5.14 + "@storybook/ui": 6.5.14 "@types/node": ^14.0.10 || ^16.0.0 babel-loader: ^8.0.0 case-sensitive-paths-webpack-plugin: ^2.3.0 @@ -2751,7 +2641,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 95e720d00e8869f8ec5e8a36f50e5da6aaecdc9a1ea306ae3d7b50f2f07b60a9eb5cb3787e88262c664c958b1f56b7f34e9bf027eb627a96d127b5771ff6a137 + checksum: 1fe525b777694c90d1837911307fa5ef3ac976387ff9b8c5116e51bfe21437c7d83504d3edb63cc5d3c9d624ba50e467f500a3477b63a879fc5d6e73310894ba languageName: node linkType: hard @@ -2774,29 +2664,29 @@ __metadata: languageName: node linkType: hard -"@storybook/node-logger@npm:6.5.13, @storybook/node-logger@npm:^6.5.9": - version: 6.5.13 - resolution: "@storybook/node-logger@npm:6.5.13" +"@storybook/node-logger@npm:6.5.14, @storybook/node-logger@npm:^6.5.9": + version: 6.5.14 + resolution: "@storybook/node-logger@npm:6.5.14" dependencies: "@types/npmlog": ^4.1.2 chalk: ^4.1.0 core-js: ^3.8.2 npmlog: ^5.0.1 pretty-hrtime: ^1.0.3 - checksum: bcd1d98822687580e39f27003e16c73e3c775cdfe6e9f8fd8fbe9f4626a82f3f63fe281f9c894f3917faa52202ccb8217916978032d27ba6dbfa9720064e7739 + checksum: d36d17816b20bf8409504f73b8fe7da2c1aa6b6f37c3cc910f544f4e3d9dfe8cfe5ab66930977413ab12a067f94477d5a1e3da8fa55c07af5dbc88160845ee47 languageName: node linkType: hard -"@storybook/preview-web@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/preview-web@npm:6.5.13" +"@storybook/preview-web@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/preview-web@npm:6.5.14" dependencies: - "@storybook/addons": 6.5.13 - "@storybook/channel-postmessage": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/core-events": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/channel-postmessage": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/store": 6.5.13 + "@storybook/store": 6.5.14 ansi-to-html: ^0.6.11 core-js: ^3.8.2 global: ^4.4.0 @@ -2810,7 +2700,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: d66d29667a936ee80d15de07bdeec0c6cb2a476fdaa59f262297f5c7dc774acb57e4779c2dd77e61f5a6d9974ac3359babe587a2cd9baf6aa7673ec949b3234d + checksum: ef4d2222feaadd71067339ec93a13279162d0d0c72545ccb52681dd38c95451bb4257f0817dd3689af5d5e5b161602c8a57bc5805cd0d6bc0e066b78a2cfdd23 languageName: node linkType: hard @@ -2833,22 +2723,22 @@ __metadata: linkType: hard "@storybook/react@npm:^6.5.9": - version: 6.5.13 - resolution: "@storybook/react@npm:6.5.13" + version: 6.5.14 + resolution: "@storybook/react@npm:6.5.14" dependencies: "@babel/preset-flow": ^7.12.1 "@babel/preset-react": ^7.12.10 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.3 - "@storybook/addons": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/core": 6.5.13 - "@storybook/core-common": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core": 6.5.14 + "@storybook/core-common": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 - "@storybook/docs-tools": 6.5.13 - "@storybook/node-logger": 6.5.13 + "@storybook/docs-tools": 6.5.14 + "@storybook/node-logger": 6.5.14 "@storybook/react-docgen-typescript-plugin": 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0 "@storybook/semver": ^7.3.2 - "@storybook/store": 6.5.13 + "@storybook/store": 6.5.14 "@types/estree": ^0.0.51 "@types/node": ^14.14.20 || ^16.0.0 "@types/webpack-env": ^1.16.0 @@ -2893,7 +2783,7 @@ __metadata: build-storybook: bin/build.js start-storybook: bin/index.js storybook-server: bin/index.js - checksum: 5a21e4e49a0aba7376dbaef5408e03537cf937d3025e735b45848bce7c9f476a025ca9af4260aa32982a3da668e14a217f66a0ec465ed820369ff2fab49b2ab3 + checksum: b94b7b3ef19d6b9e9379242fb17afd8892100cdf5dd8beac640e8dcac62618977e8189891e87e0fab40484a3fc84eeb8bc17ebd8dc7cba6b29ee7b65f8f761c8 languageName: node linkType: hard @@ -2913,11 +2803,11 @@ __metadata: languageName: node linkType: hard -"@storybook/router@npm:6.5.12": - version: 6.5.12 - resolution: "@storybook/router@npm:6.5.12" +"@storybook/router@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/router@npm:6.5.14" dependencies: - "@storybook/client-logger": 6.5.12 + "@storybook/client-logger": 6.5.14 core-js: ^3.8.2 memoizerific: ^1.11.3 qs: ^6.10.0 @@ -2925,23 +2815,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 545f4b767021b88f82eac69b9356fa5fa3a5866285c3a34fa762abc5743e3280895858aa5c820195d95a04c6768299191d4dd788a7d9fd3f17ff1d8236c0ba75 - languageName: node - linkType: hard - -"@storybook/router@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/router@npm:6.5.13" - dependencies: - "@storybook/client-logger": 6.5.13 - core-js: ^3.8.2 - memoizerific: ^1.11.3 - qs: ^6.10.0 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: ca144b2f6e3a46d5ac9d449b068d0905e9c72939c8574f095d8d7f7307b172a0c5c13f56ff08d5d5bff540292d9ed13eeecc3a4e600e282ea31e70ed763b735a + checksum: ec2550568c02f45de5307e77928eaeb39413049944e994adbc397d9c7e083ac7e110886e40517ddae40e3879c172f458167682f1d73d0bb150bc93ab9dd61514 languageName: node linkType: hard @@ -2957,34 +2831,34 @@ __metadata: languageName: node linkType: hard -"@storybook/source-loader@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/source-loader@npm:6.5.13" +"@storybook/source-loader@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/source-loader@npm:6.5.14" dependencies: - "@storybook/addons": 6.5.13 - "@storybook/client-logger": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/client-logger": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 core-js: ^3.8.2 estraverse: ^5.2.0 global: ^4.4.0 - loader-utils: ^2.0.0 + loader-utils: ^2.0.4 lodash: ^4.17.21 prettier: ">=2.2.1 <=2.3.0" regenerator-runtime: ^0.13.7 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 93da14a367a954f664d233f64bd9e7d983475e489b8fbe5c90b723ff793279ab899d86f967eb6882f7997296f4286f6f5c82c17ca83e9294c91f8e4aa0ec0a1d + checksum: 81f8908a31eb3428835a201418b89981c7ce7ece669bef2a6487b643025e45238956fe98f8fd70aa61843db0eee2c595a13e84b46835cb28b1eef59defe00740 languageName: node linkType: hard -"@storybook/store@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/store@npm:6.5.13" +"@storybook/store@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/store@npm:6.5.14" dependencies: - "@storybook/addons": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/core-events": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 "@storybook/csf": 0.0.2--canary.4566f4d.1 core-js: ^3.8.2 fast-deep-equal: ^3.1.3 @@ -3000,16 +2874,16 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 69f55927bd3569ec9d87f4351879fd07654d51524a0f9da05c64c7f7f3b50e33024f1554fa59668997e47189e9838b85b691de422ab539bd71126b56922d9381 + checksum: 7330a2492c469bbe989a522873486cbccf660080bf8d03c873029a8fa56f82976d7d71030c69aa9b2df48bf3b6fdc8063f5beecba298908783058ea675f0a159 languageName: node linkType: hard -"@storybook/telemetry@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/telemetry@npm:6.5.13" +"@storybook/telemetry@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/telemetry@npm:6.5.14" dependencies: - "@storybook/client-logger": 6.5.13 - "@storybook/core-common": 6.5.13 + "@storybook/client-logger": 6.5.14 + "@storybook/core-common": 6.5.14 chalk: ^4.1.0 core-js: ^3.8.2 detect-package-manager: ^2.0.1 @@ -3020,7 +2894,7 @@ __metadata: nanoid: ^3.3.1 read-pkg-up: ^7.0.1 regenerator-runtime: ^0.13.7 - checksum: 94ad6fb58b09c8073600ad95b2a48f476524ea7bc6155aee8e9682966a99ac875a9923ee6512252238e8c56eeef725a712e94ba87e1060d7dca9ab196a9051a6 + checksum: 6f6323fe0e0fe9301b63a951e4f2959a68847bc20b67e3910587e6979b3e0b058ce5ec1b2bc89e666b77e91211dd0bb2ed8bd23060831f599365b8f41630db53 languageName: node linkType: hard @@ -3052,49 +2926,34 @@ __metadata: languageName: node linkType: hard -"@storybook/theming@npm:6.5.12, @storybook/theming@npm:^6.0.0": - version: 6.5.12 - resolution: "@storybook/theming@npm:6.5.12" +"@storybook/theming@npm:6.5.14, @storybook/theming@npm:^6.0.0": + version: 6.5.14 + resolution: "@storybook/theming@npm:6.5.14" dependencies: - "@storybook/client-logger": 6.5.12 + "@storybook/client-logger": 6.5.14 core-js: ^3.8.2 memoizerific: ^1.11.3 regenerator-runtime: ^0.13.7 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: a982ebf88c7e1e21127febd17feebf26ac8d655f0c868bf110cbcaaef87eedb257300087618c525cb654808b590dc4b7b98dd6fec92fd76a040441d86c4b8289 + checksum: d139325dd51e8dfa58458a5c033104123b019fc02ddc899898e02de2b5d1358fd318b5def7ef82e6138420f9198e90d50b0fdfbea926987ac6852fc3a2e77c6d languageName: node linkType: hard -"@storybook/theming@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/theming@npm:6.5.13" +"@storybook/ui@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/ui@npm:6.5.14" dependencies: - "@storybook/client-logger": 6.5.13 - core-js: ^3.8.2 - memoizerific: ^1.11.3 - regenerator-runtime: ^0.13.7 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: f7a59c7d81b87f3fbf65c5eb72f5db5a5c1707236c92350d46bc7e1dcf848d57522ca5dcdd4fe19336d4611bd20727e0d900c4b591d2e8e1dd8a754cb9c56aa3 - languageName: node - linkType: hard - -"@storybook/ui@npm:6.5.13": - version: 6.5.13 - resolution: "@storybook/ui@npm:6.5.13" - dependencies: - "@storybook/addons": 6.5.13 - "@storybook/api": 6.5.13 - "@storybook/channels": 6.5.13 - "@storybook/client-logger": 6.5.13 - "@storybook/components": 6.5.13 - "@storybook/core-events": 6.5.13 - "@storybook/router": 6.5.13 + "@storybook/addons": 6.5.14 + "@storybook/api": 6.5.14 + "@storybook/channels": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/components": 6.5.14 + "@storybook/core-events": 6.5.14 + "@storybook/router": 6.5.14 "@storybook/semver": ^7.3.2 - "@storybook/theming": 6.5.13 + "@storybook/theming": 6.5.14 core-js: ^3.8.2 memoizerific: ^1.11.3 qs: ^6.10.0 @@ -3103,141 +2962,99 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: d2866987f51d945246776d42628bc2b79e701f1e59fd511bb1e590c83c4b9d9adee5c7e1a11ecb2ef12b9192613e2d576694bbc9aeb1df5545567f2aa44c0145 + checksum: 898bf81d9ab64d792bdca23b0ec465500af1e42ed5ebace5847416d0ec36d32043fb7c547452a626ce285efd966569e6bfd4f84ddd691367259553ca59538646 languageName: node linkType: hard -"@swc/core-android-arm-eabi@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-android-arm-eabi@npm:1.3.9" - dependencies: - "@swc/wasm": 1.2.122 - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@swc/core-android-arm64@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-android-arm64@npm:1.3.9" - dependencies: - "@swc/wasm": 1.2.130 - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@swc/core-darwin-arm64@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-darwin-arm64@npm:1.3.9" +"@swc/core-darwin-arm64@npm:1.3.21": + version: 1.3.21 + resolution: "@swc/core-darwin-arm64@npm:1.3.21" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-darwin-x64@npm:1.3.9" +"@swc/core-darwin-x64@npm:1.3.21": + version: 1.3.21 + resolution: "@swc/core-darwin-x64@npm:1.3.21" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-freebsd-x64@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-freebsd-x64@npm:1.3.9" - dependencies: - "@swc/wasm": 1.2.130 - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@swc/core-linux-arm-gnueabihf@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.9" - dependencies: - "@swc/wasm": 1.2.130 +"@swc/core-linux-arm-gnueabihf@npm:1.3.21": + version: 1.3.21 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.21" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.9" +"@swc/core-linux-arm64-gnu@npm:1.3.21": + version: 1.3.21 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.21" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.9" +"@swc/core-linux-arm64-musl@npm:1.3.21": + version: 1.3.21 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.21" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.9" +"@swc/core-linux-x64-gnu@npm:1.3.21": + version: 1.3.21 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.21" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-linux-x64-musl@npm:1.3.9" +"@swc/core-linux-x64-musl@npm:1.3.21": + version: 1.3.21 + resolution: "@swc/core-linux-x64-musl@npm:1.3.21" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.9" - dependencies: - "@swc/wasm": 1.2.130 +"@swc/core-win32-arm64-msvc@npm:1.3.21": + version: 1.3.21 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.21" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.9" - dependencies: - "@swc/wasm": 1.2.130 +"@swc/core-win32-ia32-msvc@npm:1.3.21": + version: 1.3.21 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.21" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.9": - version: 1.3.9 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.9" +"@swc/core-win32-x64-msvc@npm:1.3.21": + version: 1.3.21 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.21" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.3.9": - version: 1.3.9 - resolution: "@swc/core@npm:1.3.9" + version: 1.3.21 + resolution: "@swc/core@npm:1.3.21" dependencies: - "@swc/core-android-arm-eabi": 1.3.9 - "@swc/core-android-arm64": 1.3.9 - "@swc/core-darwin-arm64": 1.3.9 - "@swc/core-darwin-x64": 1.3.9 - "@swc/core-freebsd-x64": 1.3.9 - "@swc/core-linux-arm-gnueabihf": 1.3.9 - "@swc/core-linux-arm64-gnu": 1.3.9 - "@swc/core-linux-arm64-musl": 1.3.9 - "@swc/core-linux-x64-gnu": 1.3.9 - "@swc/core-linux-x64-musl": 1.3.9 - "@swc/core-win32-arm64-msvc": 1.3.9 - "@swc/core-win32-ia32-msvc": 1.3.9 - "@swc/core-win32-x64-msvc": 1.3.9 + "@swc/core-darwin-arm64": 1.3.21 + "@swc/core-darwin-x64": 1.3.21 + "@swc/core-linux-arm-gnueabihf": 1.3.21 + "@swc/core-linux-arm64-gnu": 1.3.21 + "@swc/core-linux-arm64-musl": 1.3.21 + "@swc/core-linux-x64-gnu": 1.3.21 + "@swc/core-linux-x64-musl": 1.3.21 + "@swc/core-win32-arm64-msvc": 1.3.21 + "@swc/core-win32-ia32-msvc": 1.3.21 + "@swc/core-win32-x64-msvc": 1.3.21 dependenciesMeta: - "@swc/core-android-arm-eabi": - optional: true - "@swc/core-android-arm64": - optional: true "@swc/core-darwin-arm64": optional: true "@swc/core-darwin-x64": optional: true - "@swc/core-freebsd-x64": - optional: true "@swc/core-linux-arm-gnueabihf": optional: true "@swc/core-linux-arm64-gnu": @@ -3256,21 +3073,7 @@ __metadata: optional: true bin: swcx: run_swcx.js - checksum: 761918f1bca5d494eaaafd49720717e3b3071df5bc6ef8b298a778ba4e4d20bc5a78c939a0b1c98623f9fe23e535a16a359179b72390cd1f5cbc891ea53c22fa - languageName: node - linkType: hard - -"@swc/wasm@npm:1.2.122": - version: 1.2.122 - resolution: "@swc/wasm@npm:1.2.122" - checksum: 563345370c5ad18373d3b403590ab880fe52dcd8fc8c8601be263fcd9886520b28a7f4e46236cf49ca2b136c79d4ef50c960bc34b7cdc2068118b0d84dfca1f4 - languageName: node - linkType: hard - -"@swc/wasm@npm:1.2.130": - version: 1.2.130 - resolution: "@swc/wasm@npm:1.2.130" - checksum: 02203bfef3e382c64cbbd63c138c8fdf61865e74d923b317e9d9e9f33f5a3f0a9533b5fdbc9505e76d78e864be04a82fc847eb987a1e47ccac5850146c858292 + checksum: c66cd9320c595c68b87c8d90dc9a978099dd25a84c5e9795a8c7fec95fecdd8481da82076a828880a226ad2c0e57155c0a2b97768e99dee042a74182056bda46 languageName: node linkType: hard @@ -5289,9 +5092,9 @@ __metadata: linkType: hard "decode-uri-component@npm:^0.2.0": - version: 0.2.0 - resolution: "decode-uri-component@npm:0.2.0" - checksum: f3749344ab9305ffcfe4bfe300e2dbb61fc6359e2b736812100a3b1b6db0a5668cba31a05e4b45d4d63dbf1a18dfa354cd3ca5bb3ededddabb8cd293f4404f94 + version: 0.2.2 + resolution: "decode-uri-component@npm:0.2.2" + checksum: 95476a7d28f267292ce745eac3524a9079058bbb35767b76e3ee87d42e34cd0275d2eb19d9d08c3e167f97556e8a2872747f5e65cbebcac8b0c98d83e285f139 languageName: node linkType: hard @@ -7930,25 +7733,25 @@ __metadata: languageName: node linkType: hard -"loader-utils@npm:^1.1.0, loader-utils@npm:^1.2.3": - version: 1.4.0 - resolution: "loader-utils@npm:1.4.0" +"loader-utils@npm:^1.2.3": + version: 1.4.2 + resolution: "loader-utils@npm:1.4.2" dependencies: big.js: ^5.2.2 emojis-list: ^3.0.0 json5: ^1.0.1 - checksum: d150b15e7a42ac47d935c8b484b79e44ff6ab4c75df7cc4cb9093350cf014ec0b17bdb60c5d6f91a37b8b218bd63b973e263c65944f58ca2573e402b9a27e717 + checksum: eb6fb622efc0ffd1abdf68a2022f9eac62bef8ec599cf8adb75e94d1d338381780be6278534170e99edc03380a6d29bc7eb1563c89ce17c5fed3a0b17f1ad804 languageName: node linkType: hard -"loader-utils@npm:^2.0.0": - version: 2.0.2 - resolution: "loader-utils@npm:2.0.2" +"loader-utils@npm:^2.0.0, loader-utils@npm:^2.0.3, loader-utils@npm:^2.0.4": + version: 2.0.4 + resolution: "loader-utils@npm:2.0.4" dependencies: big.js: ^5.2.2 emojis-list: ^3.0.0 json5: ^2.1.2 - checksum: 9078d1ed47cadc57f4c6ddbdb2add324ee7da544cea41de3b7f1128e8108fcd41cd3443a85b7ee8d7d8ac439148aa221922774efe4cf87506d4fb054d5889303 + checksum: a5281f5fff1eaa310ad5e1164095689443630f3411e927f95031ab4fb83b4a98f388185bb1fe949e8ab8d4247004336a625e9255c22122b815bb9a4c5d8fc3b7 languageName: node linkType: hard @@ -8465,16 +8268,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^3.0.0, minipass@npm:^3.1.1": - version: 3.3.4 - resolution: "minipass@npm:3.3.4" - dependencies: - yallist: ^4.0.0 - checksum: 5d95a7738c54852ba78d484141e850c792e062666a2d0c681a5ac1021275beb7e1acb077e59f9523ff1defb80901aea4e30fac10ded9a20a25d819a42916ef1b - languageName: node - linkType: hard - -"minipass@npm:^3.1.6": +"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": version: 3.3.5 resolution: "minipass@npm:3.3.5" dependencies: @@ -9812,25 +9606,25 @@ __metadata: linkType: hard "react-hot-loader@npm:^4.13.0": - version: 4.13.0 - resolution: "react-hot-loader@npm:4.13.0" + version: 4.13.1 + resolution: "react-hot-loader@npm:4.13.1" dependencies: fast-levenshtein: ^2.0.6 global: ^4.3.0 hoist-non-react-statics: ^3.3.0 - loader-utils: ^1.1.0 + loader-utils: ^2.0.3 prop-types: ^15.6.1 react-lifecycles-compat: ^3.0.4 shallowequal: ^1.1.0 source-map: ^0.7.3 peerDependencies: - "@types/react": "^15.0.0 || ^16.0.0 || ^17.0.0 " - react: "^15.0.0 || ^16.0.0 || ^17.0.0 " - react-dom: "^15.0.0 || ^16.0.0 || ^17.0.0 " + "@types/react": ^15.0.0 || ^16.0.0 || ^17.0.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 peerDependenciesMeta: "@types/react": optional: true - checksum: effdbf4644ce912ae20ad94be62083970c74b26a59fe24ed0024cf73190a5b3edf59650cb693bdd7b70791df8ab8530de273d73b895c4831a91da8a76683e3a3 + checksum: f90890d5160dcb2bfae4022cba065d8e5c26b86f34c4604cbbdd39f5e4dfd82c05c317bb05ef3d4da2aa0be7e8e205c905daea1af40d3a7ef0d07c3d289da11c languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index 0957bb9edc..6311ccc002 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,17 +21,6 @@ __metadata: languageName: node linkType: hard -"@apidevtools/json-schema-ref-parser@npm:9.0.6": - version: 9.0.6 - resolution: "@apidevtools/json-schema-ref-parser@npm:9.0.6" - dependencies: - "@jsdevtools/ono": ^7.1.3 - call-me-maybe: ^1.0.1 - js-yaml: ^3.13.1 - checksum: c7ff53623ab8a9dd221772a5757fa0b9e5167a5ac3a71c23596634bae6efc85d8efcdebbe17f73ee5c027ea5afc48c705e8a720f02c4909f9a357d8027040b7b - languageName: node - linkType: hard - "@apidevtools/json-schema-ref-parser@npm:^9.0.6": version: 9.1.0 resolution: "@apidevtools/json-schema-ref-parser@npm:9.1.0" @@ -44,37 +33,6 @@ __metadata: languageName: node linkType: hard -"@apidevtools/openapi-schemas@npm:^2.1.0": - version: 2.1.0 - resolution: "@apidevtools/openapi-schemas@npm:2.1.0" - checksum: 4a8f64935b9049ef21e41fa4b188f39f6bc3f5291cebd451701db1115451ccb246a739e46cc5ce9ecdec781671431db40db7851acdac84a990a45756e0f32de3 - languageName: node - linkType: hard - -"@apidevtools/swagger-methods@npm:^3.0.2": - version: 3.0.2 - resolution: "@apidevtools/swagger-methods@npm:3.0.2" - checksum: d06b1ac5c1956613c4c6be695612ef860cd4e962b93a509ca551735a328a856cae1e33399cac1dcbf8333ba22b231746f3586074769ef0e172cf549ec9e7eaae - languageName: node - linkType: hard - -"@apidevtools/swagger-parser@npm:^10.1.0": - version: 10.1.0 - resolution: "@apidevtools/swagger-parser@npm:10.1.0" - dependencies: - "@apidevtools/json-schema-ref-parser": 9.0.6 - "@apidevtools/openapi-schemas": ^2.1.0 - "@apidevtools/swagger-methods": ^3.0.2 - "@jsdevtools/ono": ^7.1.3 - ajv: ^8.6.3 - ajv-draft-04: ^1.0.0 - call-me-maybe: ^1.0.1 - peerDependencies: - openapi-types: ">=7" - checksum: c7c923755bd025ee2cae97e1cfd525538523ba74c341a0ac814c023ffe5e63fc2d997539a8ccf9a0fcec41a2d6337d40cc5735acb991ddcbb415853a241908d1 - languageName: node - linkType: hard - "@apollo/explorer@npm:^1.1.1": version: 1.2.0 resolution: "@apollo/explorer@npm:1.2.0" @@ -291,9 +249,9 @@ __metadata: languageName: node linkType: hard -"@asyncapi/react-component@npm:1.0.0-next.43": - version: 1.0.0-next.43 - resolution: "@asyncapi/react-component@npm:1.0.0-next.43" +"@asyncapi/react-component@npm:1.0.0-next.44": + version: 1.0.0-next.44 + resolution: "@asyncapi/react-component@npm:1.0.0-next.44" dependencies: "@asyncapi/avro-schema-parser": ^0.3.0 "@asyncapi/openapi-schema-parser": ^2.0.0 @@ -307,7 +265,7 @@ __metadata: peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: d19adc2b6e674633457da28d38636df4cf9891f4038827b52f2737699d064bcf0d3be707e9f3284852d1bb507f3f36df9ec3bb02fd6f901cfda7a1f49ea9c5da + checksum: 1d96db3bc6a4655884969a7639467836376c028df1a00f8930adf4e1b6ecb43e6a2d61df5c68e63d6c105d319fecb3f2ac0e4146274a81a874fbde17f4dbac1c languageName: node linkType: hard @@ -318,6 +276,1341 @@ __metadata: languageName: node linkType: hard +"@aws-crypto/crc32@npm:2.0.0": + version: 2.0.0 + resolution: "@aws-crypto/crc32@npm:2.0.0" + dependencies: + "@aws-crypto/util": ^2.0.0 + "@aws-sdk/types": ^3.1.0 + tslib: ^1.11.1 + checksum: 88ab906da8304a430c655496e363835f3c7ca870db0dec50bb9d53ed0f446337de60c85ba7baa4528c8363bee708474785649262ebfde23a1e099eb69318b53e + languageName: node + linkType: hard + +"@aws-crypto/crc32c@npm:2.0.0": + version: 2.0.0 + resolution: "@aws-crypto/crc32c@npm:2.0.0" + dependencies: + "@aws-crypto/util": ^2.0.0 + "@aws-sdk/types": ^3.1.0 + tslib: ^1.11.1 + checksum: 776e1e61b3bde018b815d3973774a4ec3cd88c282046e49bb5a2625ac7db923068aad708a8e4c21b62a80e50a5c58324a32621b9ba82b7b2ecab66370c4fe893 + languageName: node + linkType: hard + +"@aws-crypto/ie11-detection@npm:^2.0.0": + version: 2.0.2 + resolution: "@aws-crypto/ie11-detection@npm:2.0.2" + dependencies: + tslib: ^1.11.1 + checksum: 713293deea8eefd3ab43dc05e62228571d27754e7293f8ec2fd8a0c693fbbfc55213e6599387776e3cdbc951965dc62e24e92b9c4a853e4a50d00ae6a9f6b2bd + languageName: node + linkType: hard + +"@aws-crypto/sha1-browser@npm:2.0.0": + version: 2.0.0 + resolution: "@aws-crypto/sha1-browser@npm:2.0.0" + dependencies: + "@aws-crypto/ie11-detection": ^2.0.0 + "@aws-crypto/supports-web-crypto": ^2.0.0 + "@aws-sdk/types": ^3.1.0 + "@aws-sdk/util-locate-window": ^3.0.0 + "@aws-sdk/util-utf8-browser": ^3.0.0 + tslib: ^1.11.1 + checksum: 72c0b24800cd79328fef934553e7a5d5929c90877d7e9a661614542dd29d0d99ee1594bd59fc028ee9ec595d77df56645a396c6336cff3c7784a564302d4a254 + languageName: node + linkType: hard + +"@aws-crypto/sha256-browser@npm:2.0.0": + version: 2.0.0 + resolution: "@aws-crypto/sha256-browser@npm:2.0.0" + dependencies: + "@aws-crypto/ie11-detection": ^2.0.0 + "@aws-crypto/sha256-js": ^2.0.0 + "@aws-crypto/supports-web-crypto": ^2.0.0 + "@aws-crypto/util": ^2.0.0 + "@aws-sdk/types": ^3.1.0 + "@aws-sdk/util-locate-window": ^3.0.0 + "@aws-sdk/util-utf8-browser": ^3.0.0 + tslib: ^1.11.1 + checksum: 7bc1ff042d0c53a46c0fc3824bd97fb3ed1df7dc030b8a995889471052860b8c8ade469c97866fafd8249a3144d0f48b0f1054f357e2b403606009381c4b8f0e + languageName: node + linkType: hard + +"@aws-crypto/sha256-js@npm:2.0.0": + version: 2.0.0 + resolution: "@aws-crypto/sha256-js@npm:2.0.0" + dependencies: + "@aws-crypto/util": ^2.0.0 + "@aws-sdk/types": ^3.1.0 + tslib: ^1.11.1 + checksum: e4abf9baec6bed19d380f92a999a41ac5bdd8890dfd45971d29054c298854c5b7087e7de633413f2e64618ef8238ccf4c0b75797c73063c74bbba3cb5d8b2581 + languageName: node + linkType: hard + +"@aws-crypto/sha256-js@npm:^2.0.0": + version: 2.0.2 + resolution: "@aws-crypto/sha256-js@npm:2.0.2" + dependencies: + "@aws-crypto/util": ^2.0.2 + "@aws-sdk/types": ^3.110.0 + tslib: ^1.11.1 + checksum: 9125ec65a2b05fce908ac2289ba97b995a299f2d717684804211df8e8bcffd8cd9b8861582240655b88f2255c46fcee34026f75c057ffb22f44b6a76cd43f65a + languageName: node + linkType: hard + +"@aws-crypto/supports-web-crypto@npm:^2.0.0": + version: 2.0.2 + resolution: "@aws-crypto/supports-web-crypto@npm:2.0.2" + dependencies: + tslib: ^1.11.1 + checksum: 03d04d29292dc1b76db9bc6becd05f52fa79adee0ec084f971b0767f7e73250dd0422bea57636015f8c27f38aefcd1d9c58800a4749cf35339296c8d670f3ccb + languageName: node + linkType: hard + +"@aws-crypto/util@npm:^2.0.0, @aws-crypto/util@npm:^2.0.2": + version: 2.0.2 + resolution: "@aws-crypto/util@npm:2.0.2" + dependencies: + "@aws-sdk/types": ^3.110.0 + "@aws-sdk/util-utf8-browser": ^3.0.0 + tslib: ^1.11.1 + checksum: 13cb33a39005b09c062398d361043c2224bc8ba42b1432bad52e15bc4bf9ffad4facdddc394b3cc71b3fb8d86a7ec325fd1afa107b5fde0dab84a7e32d311d7f + languageName: node + linkType: hard + +"@aws-sdk/abort-controller@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/abort-controller@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 06528afdab15ed4f088b313e3e42c4ed9b46f4887a2f6f75aabe98830027510e5541965cefa3af1a9f0315ae4b37bdc16b6a9c80b7fca65719b47c8909aad260 + languageName: node + linkType: hard + +"@aws-sdk/chunked-blob-reader-native@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/chunked-blob-reader-native@npm:3.208.0" + dependencies: + "@aws-sdk/util-base64": 3.208.0 + tslib: ^2.3.1 + checksum: 6df294c66d6acaa3de2c5f3444a62577d0effa451a65b6a1606df538cfc865d257f01c74e711b36741a2c59f43385bd393c07c8562fe5101d6069b8c2646da53 + languageName: node + linkType: hard + +"@aws-sdk/chunked-blob-reader@npm:3.188.0": + version: 3.188.0 + resolution: "@aws-sdk/chunked-blob-reader@npm:3.188.0" + dependencies: + tslib: ^2.3.1 + checksum: d6becebc6d77a63dd84601806c5f04bc9d0e418368fb2d593d786738119091e4d2e40721751be66260063fe7bb1c7ee2a5e075fa731eb8307722b90f6ce809dc + languageName: node + linkType: hard + +"@aws-sdk/client-cognito-identity@npm:3.223.0": + version: 3.223.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.223.0" + dependencies: + "@aws-crypto/sha256-browser": 2.0.0 + "@aws-crypto/sha256-js": 2.0.0 + "@aws-sdk/client-sts": 3.223.0 + "@aws-sdk/config-resolver": 3.222.0 + "@aws-sdk/credential-provider-node": 3.223.0 + "@aws-sdk/fetch-http-handler": 3.222.0 + "@aws-sdk/hash-node": 3.222.0 + "@aws-sdk/invalid-dependency": 3.222.0 + "@aws-sdk/middleware-content-length": 3.222.0 + "@aws-sdk/middleware-endpoint": 3.222.0 + "@aws-sdk/middleware-host-header": 3.222.0 + "@aws-sdk/middleware-logger": 3.222.0 + "@aws-sdk/middleware-recursion-detection": 3.222.0 + "@aws-sdk/middleware-retry": 3.222.0 + "@aws-sdk/middleware-serde": 3.222.0 + "@aws-sdk/middleware-signing": 3.222.0 + "@aws-sdk/middleware-stack": 3.222.0 + "@aws-sdk/middleware-user-agent": 3.222.0 + "@aws-sdk/node-config-provider": 3.222.0 + "@aws-sdk/node-http-handler": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/smithy-client": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/url-parser": 3.222.0 + "@aws-sdk/util-base64": 3.208.0 + "@aws-sdk/util-body-length-browser": 3.188.0 + "@aws-sdk/util-body-length-node": 3.208.0 + "@aws-sdk/util-defaults-mode-browser": 3.222.0 + "@aws-sdk/util-defaults-mode-node": 3.222.0 + "@aws-sdk/util-endpoints": 3.222.0 + "@aws-sdk/util-user-agent-browser": 3.222.0 + "@aws-sdk/util-user-agent-node": 3.222.0 + "@aws-sdk/util-utf8-browser": 3.188.0 + "@aws-sdk/util-utf8-node": 3.208.0 + tslib: ^2.3.1 + checksum: ada894c3fbe943044617d719dc947af51546dbfea72e5b63f23c6230677c39dc0355248ed1ba119a29f2d4b7bc64e28400880b580a263bcd45d05fdf45007ad7 + languageName: node + linkType: hard + +"@aws-sdk/client-s3@npm:^3.208.0": + version: 3.223.0 + resolution: "@aws-sdk/client-s3@npm:3.223.0" + dependencies: + "@aws-crypto/sha1-browser": 2.0.0 + "@aws-crypto/sha256-browser": 2.0.0 + "@aws-crypto/sha256-js": 2.0.0 + "@aws-sdk/client-sts": 3.223.0 + "@aws-sdk/config-resolver": 3.222.0 + "@aws-sdk/credential-provider-node": 3.223.0 + "@aws-sdk/eventstream-serde-browser": 3.222.0 + "@aws-sdk/eventstream-serde-config-resolver": 3.222.0 + "@aws-sdk/eventstream-serde-node": 3.222.0 + "@aws-sdk/fetch-http-handler": 3.222.0 + "@aws-sdk/hash-blob-browser": 3.222.0 + "@aws-sdk/hash-node": 3.222.0 + "@aws-sdk/hash-stream-node": 3.222.0 + "@aws-sdk/invalid-dependency": 3.222.0 + "@aws-sdk/md5-js": 3.222.0 + "@aws-sdk/middleware-bucket-endpoint": 3.222.0 + "@aws-sdk/middleware-content-length": 3.222.0 + "@aws-sdk/middleware-endpoint": 3.222.0 + "@aws-sdk/middleware-expect-continue": 3.222.0 + "@aws-sdk/middleware-flexible-checksums": 3.222.0 + "@aws-sdk/middleware-host-header": 3.222.0 + "@aws-sdk/middleware-location-constraint": 3.222.0 + "@aws-sdk/middleware-logger": 3.222.0 + "@aws-sdk/middleware-recursion-detection": 3.222.0 + "@aws-sdk/middleware-retry": 3.222.0 + "@aws-sdk/middleware-sdk-s3": 3.222.0 + "@aws-sdk/middleware-serde": 3.222.0 + "@aws-sdk/middleware-signing": 3.222.0 + "@aws-sdk/middleware-ssec": 3.222.0 + "@aws-sdk/middleware-stack": 3.222.0 + "@aws-sdk/middleware-user-agent": 3.222.0 + "@aws-sdk/node-config-provider": 3.222.0 + "@aws-sdk/node-http-handler": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/signature-v4-multi-region": 3.222.0 + "@aws-sdk/smithy-client": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/url-parser": 3.222.0 + "@aws-sdk/util-base64": 3.208.0 + "@aws-sdk/util-body-length-browser": 3.188.0 + "@aws-sdk/util-body-length-node": 3.208.0 + "@aws-sdk/util-defaults-mode-browser": 3.222.0 + "@aws-sdk/util-defaults-mode-node": 3.222.0 + "@aws-sdk/util-endpoints": 3.222.0 + "@aws-sdk/util-stream-browser": 3.222.0 + "@aws-sdk/util-stream-node": 3.222.0 + "@aws-sdk/util-user-agent-browser": 3.222.0 + "@aws-sdk/util-user-agent-node": 3.222.0 + "@aws-sdk/util-utf8-browser": 3.188.0 + "@aws-sdk/util-utf8-node": 3.208.0 + "@aws-sdk/util-waiter": 3.222.0 + "@aws-sdk/xml-builder": 3.201.0 + fast-xml-parser: 4.0.11 + tslib: ^2.3.1 + checksum: 52f5fe06c0740db4ccfbe61010654e02ecc0e994cdd6feb8b95a49e507a2813fe2889bb7cf12977febfe3d96333a962c16e8d26a77df80a9bd23a635dcc0f505 + languageName: node + linkType: hard + +"@aws-sdk/client-sqs@npm:^3.208.0": + version: 3.223.0 + resolution: "@aws-sdk/client-sqs@npm:3.223.0" + dependencies: + "@aws-crypto/sha256-browser": 2.0.0 + "@aws-crypto/sha256-js": 2.0.0 + "@aws-sdk/client-sts": 3.223.0 + "@aws-sdk/config-resolver": 3.222.0 + "@aws-sdk/credential-provider-node": 3.223.0 + "@aws-sdk/fetch-http-handler": 3.222.0 + "@aws-sdk/hash-node": 3.222.0 + "@aws-sdk/invalid-dependency": 3.222.0 + "@aws-sdk/md5-js": 3.222.0 + "@aws-sdk/middleware-content-length": 3.222.0 + "@aws-sdk/middleware-endpoint": 3.222.0 + "@aws-sdk/middleware-host-header": 3.222.0 + "@aws-sdk/middleware-logger": 3.222.0 + "@aws-sdk/middleware-recursion-detection": 3.222.0 + "@aws-sdk/middleware-retry": 3.222.0 + "@aws-sdk/middleware-sdk-sqs": 3.222.0 + "@aws-sdk/middleware-serde": 3.222.0 + "@aws-sdk/middleware-signing": 3.222.0 + "@aws-sdk/middleware-stack": 3.222.0 + "@aws-sdk/middleware-user-agent": 3.222.0 + "@aws-sdk/node-config-provider": 3.222.0 + "@aws-sdk/node-http-handler": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/smithy-client": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/url-parser": 3.222.0 + "@aws-sdk/util-base64": 3.208.0 + "@aws-sdk/util-body-length-browser": 3.188.0 + "@aws-sdk/util-body-length-node": 3.208.0 + "@aws-sdk/util-defaults-mode-browser": 3.222.0 + "@aws-sdk/util-defaults-mode-node": 3.222.0 + "@aws-sdk/util-endpoints": 3.222.0 + "@aws-sdk/util-user-agent-browser": 3.222.0 + "@aws-sdk/util-user-agent-node": 3.222.0 + "@aws-sdk/util-utf8-browser": 3.188.0 + "@aws-sdk/util-utf8-node": 3.208.0 + fast-xml-parser: 4.0.11 + tslib: ^2.3.1 + checksum: 3f5e5052e02c33590199dcdc37df66692b5330e15afaaa5868b55ccb8a085f2a839dce5bda54270e75fe660b0be6fac3071722922ddb12922dd1f162912fe77e + languageName: node + linkType: hard + +"@aws-sdk/client-sso-oidc@npm:3.223.0": + version: 3.223.0 + resolution: "@aws-sdk/client-sso-oidc@npm:3.223.0" + dependencies: + "@aws-crypto/sha256-browser": 2.0.0 + "@aws-crypto/sha256-js": 2.0.0 + "@aws-sdk/config-resolver": 3.222.0 + "@aws-sdk/fetch-http-handler": 3.222.0 + "@aws-sdk/hash-node": 3.222.0 + "@aws-sdk/invalid-dependency": 3.222.0 + "@aws-sdk/middleware-content-length": 3.222.0 + "@aws-sdk/middleware-endpoint": 3.222.0 + "@aws-sdk/middleware-host-header": 3.222.0 + "@aws-sdk/middleware-logger": 3.222.0 + "@aws-sdk/middleware-recursion-detection": 3.222.0 + "@aws-sdk/middleware-retry": 3.222.0 + "@aws-sdk/middleware-serde": 3.222.0 + "@aws-sdk/middleware-stack": 3.222.0 + "@aws-sdk/middleware-user-agent": 3.222.0 + "@aws-sdk/node-config-provider": 3.222.0 + "@aws-sdk/node-http-handler": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/smithy-client": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/url-parser": 3.222.0 + "@aws-sdk/util-base64": 3.208.0 + "@aws-sdk/util-body-length-browser": 3.188.0 + "@aws-sdk/util-body-length-node": 3.208.0 + "@aws-sdk/util-defaults-mode-browser": 3.222.0 + "@aws-sdk/util-defaults-mode-node": 3.222.0 + "@aws-sdk/util-endpoints": 3.222.0 + "@aws-sdk/util-user-agent-browser": 3.222.0 + "@aws-sdk/util-user-agent-node": 3.222.0 + "@aws-sdk/util-utf8-browser": 3.188.0 + "@aws-sdk/util-utf8-node": 3.208.0 + tslib: ^2.3.1 + checksum: ab5b6142d897ec48a3bb246577c23d1b6e26ee228c7ce264f826d10f88caa83e03f0dac9d88e361c70b778ceef45f0ebd9712cd03ca5b60ab240a3d4382ecb7c + languageName: node + linkType: hard + +"@aws-sdk/client-sso@npm:3.223.0": + version: 3.223.0 + resolution: "@aws-sdk/client-sso@npm:3.223.0" + dependencies: + "@aws-crypto/sha256-browser": 2.0.0 + "@aws-crypto/sha256-js": 2.0.0 + "@aws-sdk/config-resolver": 3.222.0 + "@aws-sdk/fetch-http-handler": 3.222.0 + "@aws-sdk/hash-node": 3.222.0 + "@aws-sdk/invalid-dependency": 3.222.0 + "@aws-sdk/middleware-content-length": 3.222.0 + "@aws-sdk/middleware-endpoint": 3.222.0 + "@aws-sdk/middleware-host-header": 3.222.0 + "@aws-sdk/middleware-logger": 3.222.0 + "@aws-sdk/middleware-recursion-detection": 3.222.0 + "@aws-sdk/middleware-retry": 3.222.0 + "@aws-sdk/middleware-serde": 3.222.0 + "@aws-sdk/middleware-stack": 3.222.0 + "@aws-sdk/middleware-user-agent": 3.222.0 + "@aws-sdk/node-config-provider": 3.222.0 + "@aws-sdk/node-http-handler": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/smithy-client": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/url-parser": 3.222.0 + "@aws-sdk/util-base64": 3.208.0 + "@aws-sdk/util-body-length-browser": 3.188.0 + "@aws-sdk/util-body-length-node": 3.208.0 + "@aws-sdk/util-defaults-mode-browser": 3.222.0 + "@aws-sdk/util-defaults-mode-node": 3.222.0 + "@aws-sdk/util-endpoints": 3.222.0 + "@aws-sdk/util-user-agent-browser": 3.222.0 + "@aws-sdk/util-user-agent-node": 3.222.0 + "@aws-sdk/util-utf8-browser": 3.188.0 + "@aws-sdk/util-utf8-node": 3.208.0 + tslib: ^2.3.1 + checksum: 856e3745182906d274dc894baa7d853966b0074f89ea892f29132e1cefdf49f267f1d0fffaa2de3de352d19ff3963d59290602057bd4510731e0156199bed664 + languageName: node + linkType: hard + +"@aws-sdk/client-sts@npm:3.223.0": + version: 3.223.0 + resolution: "@aws-sdk/client-sts@npm:3.223.0" + dependencies: + "@aws-crypto/sha256-browser": 2.0.0 + "@aws-crypto/sha256-js": 2.0.0 + "@aws-sdk/config-resolver": 3.222.0 + "@aws-sdk/credential-provider-node": 3.223.0 + "@aws-sdk/fetch-http-handler": 3.222.0 + "@aws-sdk/hash-node": 3.222.0 + "@aws-sdk/invalid-dependency": 3.222.0 + "@aws-sdk/middleware-content-length": 3.222.0 + "@aws-sdk/middleware-endpoint": 3.222.0 + "@aws-sdk/middleware-host-header": 3.222.0 + "@aws-sdk/middleware-logger": 3.222.0 + "@aws-sdk/middleware-recursion-detection": 3.222.0 + "@aws-sdk/middleware-retry": 3.222.0 + "@aws-sdk/middleware-sdk-sts": 3.222.0 + "@aws-sdk/middleware-serde": 3.222.0 + "@aws-sdk/middleware-signing": 3.222.0 + "@aws-sdk/middleware-stack": 3.222.0 + "@aws-sdk/middleware-user-agent": 3.222.0 + "@aws-sdk/node-config-provider": 3.222.0 + "@aws-sdk/node-http-handler": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/smithy-client": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/url-parser": 3.222.0 + "@aws-sdk/util-base64": 3.208.0 + "@aws-sdk/util-body-length-browser": 3.188.0 + "@aws-sdk/util-body-length-node": 3.208.0 + "@aws-sdk/util-defaults-mode-browser": 3.222.0 + "@aws-sdk/util-defaults-mode-node": 3.222.0 + "@aws-sdk/util-endpoints": 3.222.0 + "@aws-sdk/util-user-agent-browser": 3.222.0 + "@aws-sdk/util-user-agent-node": 3.222.0 + "@aws-sdk/util-utf8-browser": 3.188.0 + "@aws-sdk/util-utf8-node": 3.208.0 + fast-xml-parser: 4.0.11 + tslib: ^2.3.1 + checksum: 6976345f839b0028a0e83f6d11e3323da68cef103ae5381299c6d75df25a1ef336b83023c79896242fbc4b7d2a27f3deebb1b1922e815e6cc7780f3eee302210 + languageName: node + linkType: hard + +"@aws-sdk/config-resolver@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/config-resolver@npm:3.222.0" + dependencies: + "@aws-sdk/signature-v4": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-config-provider": 3.208.0 + "@aws-sdk/util-middleware": 3.222.0 + tslib: ^2.3.1 + checksum: 8a2524cf1e8bd9d7acf82a039a1da7de9a7f9017d7a0dd913024e74d126509bdaa0e2b2bfc0b9ba5a9aee83af4022e2573f59d675997cc81b726d8300afd22d5 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-cognito-identity@npm:3.223.0": + version: 3.223.0 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.223.0" + dependencies: + "@aws-sdk/client-cognito-identity": 3.223.0 + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: e0f03110b120699ef9c458edd5b1a248f5e3f360931b2e8be7ebb7fc33da51ac1c3fdf96bb05a755a2d303c00c7effee7a582706da2333b4ee43c0040ff551a5 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-env@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/credential-provider-env@npm:3.222.0" + dependencies: + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: ae524102f47c8ac471231d990b63d3a3272826f94efd9a37d6a5ccc1c42db1edc86a69c67743eeba16fbead5b6800578de1a43831b99005e6d5b974acdc553d0 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-imds@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/credential-provider-imds@npm:3.222.0" + dependencies: + "@aws-sdk/node-config-provider": 3.222.0 + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/url-parser": 3.222.0 + tslib: ^2.3.1 + checksum: 41e5ee3819896063fdd3054d9a21d954b6904eaf66b4fc1385407de18dab7417704f2c2ebfad27330692fda629cc8878a0d7291a67f84ab4880a1c1a4a4d3c22 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:3.223.0": + version: 3.223.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.223.0" + dependencies: + "@aws-sdk/credential-provider-env": 3.222.0 + "@aws-sdk/credential-provider-imds": 3.222.0 + "@aws-sdk/credential-provider-sso": 3.223.0 + "@aws-sdk/credential-provider-web-identity": 3.222.0 + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/shared-ini-file-loader": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: f407fe591999f1da1d5580b182af261ceeb4455da9a53d18ae871c9cf244e6e72fa10ef28036419790e2b5787d64c5de5b4daf4c7f69acb866894410baa880d0 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-node@npm:3.223.0": + version: 3.223.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.223.0" + dependencies: + "@aws-sdk/credential-provider-env": 3.222.0 + "@aws-sdk/credential-provider-imds": 3.222.0 + "@aws-sdk/credential-provider-ini": 3.223.0 + "@aws-sdk/credential-provider-process": 3.222.0 + "@aws-sdk/credential-provider-sso": 3.223.0 + "@aws-sdk/credential-provider-web-identity": 3.222.0 + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/shared-ini-file-loader": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 6ce5f88f75a6e879344ea76fa2b390c779d74f1b3657fa15367b15797b09d9713ef0d2f14c95421254a6bc0b8bde1acd2634978a6549e02a74c33160ae6130fd + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-process@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/credential-provider-process@npm:3.222.0" + dependencies: + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/shared-ini-file-loader": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 9f51373b68be65f4ec1a1412796070c4c8570729ad1b2e6882c802b152d6ac54c2877df8049e7e483fe85af94fdd19bda6145ebe9130e7a6502f851ae8db1855 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-sso@npm:3.223.0": + version: 3.223.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.223.0" + dependencies: + "@aws-sdk/client-sso": 3.223.0 + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/shared-ini-file-loader": 3.222.0 + "@aws-sdk/token-providers": 3.223.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 4593a94dc86ef7218517061cf875285c2c6087f1d4a1199442055e6165701f11f9921612a04ab3c1b3f29b3132d46a8ce8cd26070fad132509e20eca1b89f217 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-web-identity@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.222.0" + dependencies: + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 4cae5d24b048decc17e44cef846635d7dfc3e2941b26391b144677a355f2ed4b3b798e87e14cc04959283c4f4914267edaaf8d92e58cd61a4a529551e61459ae + languageName: node + linkType: hard + +"@aws-sdk/credential-providers@npm:^3.208.0": + version: 3.223.0 + resolution: "@aws-sdk/credential-providers@npm:3.223.0" + dependencies: + "@aws-sdk/client-cognito-identity": 3.223.0 + "@aws-sdk/client-sso": 3.223.0 + "@aws-sdk/client-sts": 3.223.0 + "@aws-sdk/credential-provider-cognito-identity": 3.223.0 + "@aws-sdk/credential-provider-env": 3.222.0 + "@aws-sdk/credential-provider-imds": 3.222.0 + "@aws-sdk/credential-provider-ini": 3.223.0 + "@aws-sdk/credential-provider-node": 3.223.0 + "@aws-sdk/credential-provider-process": 3.222.0 + "@aws-sdk/credential-provider-sso": 3.223.0 + "@aws-sdk/credential-provider-web-identity": 3.222.0 + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/shared-ini-file-loader": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 289d043f41460751e90c3adc323f3a56e517f67c0a7f8889249e0969b3302f19d093eef4a10d3528bdf34f1043f95fa41555ed5c91af4d2ca636cded1775c81c + languageName: node + linkType: hard + +"@aws-sdk/eventstream-codec@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/eventstream-codec@npm:3.222.0" + dependencies: + "@aws-crypto/crc32": 2.0.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-hex-encoding": 3.201.0 + tslib: ^2.3.1 + checksum: e41eebf3117623b26a825de7faebe3ce7e0f8aeaf01044902393581d36887879bdd5e73f20a708ef4031a6cdf0de2b845442cd83ac1b0f02ae9c056778c93113 + languageName: node + linkType: hard + +"@aws-sdk/eventstream-serde-browser@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/eventstream-serde-browser@npm:3.222.0" + dependencies: + "@aws-sdk/eventstream-serde-universal": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 7bddf8d48b99615e3e03f051eee4dbb92cd84a23e688c83a7a2d4b8383ece4b59864c2d337d581d3eeaa62f692f912be20091c6b0bb82631d4681fb83d3898b9 + languageName: node + linkType: hard + +"@aws-sdk/eventstream-serde-config-resolver@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/eventstream-serde-config-resolver@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 77f133dea7ae542cdcd8c2e4533ab862ddf7167a328c19d61ed360733ed3f9f3b4dfd66a52431300dfd87babafc0db8241e27f66178cb90973778b4ac06c2e0a + languageName: node + linkType: hard + +"@aws-sdk/eventstream-serde-node@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/eventstream-serde-node@npm:3.222.0" + dependencies: + "@aws-sdk/eventstream-serde-universal": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: f18b1254ffffaee8acd7f5f1104969ca29248f797e8a9b018aaf4945e5a6b3bd1d6f46078a878232b1e99133e963e9ad8d840389d3fc9e8b0edc00e767c69d9f + languageName: node + linkType: hard + +"@aws-sdk/eventstream-serde-universal@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/eventstream-serde-universal@npm:3.222.0" + dependencies: + "@aws-sdk/eventstream-codec": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: e3a02b2edc0245fce5137adfaf945c969a2285e5f681752ef257c837f578ece4872af3d7ce3ea2b6776761d70d77151034e843d98f9bbed9523cf3e533f49994 + languageName: node + linkType: hard + +"@aws-sdk/fetch-http-handler@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/fetch-http-handler@npm:3.208.0" + dependencies: + "@aws-sdk/protocol-http": 3.208.0 + "@aws-sdk/querystring-builder": 3.208.0 + "@aws-sdk/types": 3.208.0 + "@aws-sdk/util-base64": 3.208.0 + tslib: ^2.3.1 + checksum: a0016ec854d247e28ba7a86e72d6915854b33d489e88e5d26c8e3843704a3978129b4b52619c9810acd19c47dbf9b4359be17ad939e0be676b4af9cb21952abf + languageName: node + linkType: hard + +"@aws-sdk/fetch-http-handler@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/fetch-http-handler@npm:3.222.0" + dependencies: + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/querystring-builder": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-base64": 3.208.0 + tslib: ^2.3.1 + checksum: b6fa1a03e8e7fa4aea13f9aa154968b93ce1c6eca8f2f3fd58961bad167a720dc999cc62270bdf707d21e2136d22171f1099b4b326ccbcff52bea59a5a278630 + languageName: node + linkType: hard + +"@aws-sdk/hash-blob-browser@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/hash-blob-browser@npm:3.222.0" + dependencies: + "@aws-sdk/chunked-blob-reader": 3.188.0 + "@aws-sdk/chunked-blob-reader-native": 3.208.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: c70be9c57ebfb3c549be807af61a77242952f39696eca2c38c77b6cde9afe8f83f92c8467cde9224f4da97e97635a3987759cd3996fd07ee23a1cf67dafc0d92 + languageName: node + linkType: hard + +"@aws-sdk/hash-node@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/hash-node@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-buffer-from": 3.208.0 + tslib: ^2.3.1 + checksum: 25ad5a33eb74d7a6a43f1d9d668a268e1ac9d1e5f394395784fc6011d10448630f79ea1f5c5dfbed249556cd4dec2f091d0913eca82700bdea1f64564c6512ae + languageName: node + linkType: hard + +"@aws-sdk/hash-stream-node@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/hash-stream-node@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 6c58512030bb6e10601dda4de1efa0b61b2d6d9828018682c751a5a10a2d52e7931181818b51d8bf9cdf8ec0ef3fc2d93f0ef71ede51e91abc3b4f04a777ac4f + languageName: node + linkType: hard + +"@aws-sdk/invalid-dependency@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/invalid-dependency@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 1b6f3c2330f57506d8431cd7d295ab483e739a8be21556b5495cf3b0501776e0b286ddb5f049acd8814f46d44380a3c4ec0790c3226cbe8dcea03998c4bc1fd7 + languageName: node + linkType: hard + +"@aws-sdk/is-array-buffer@npm:3.201.0": + version: 3.201.0 + resolution: "@aws-sdk/is-array-buffer@npm:3.201.0" + dependencies: + tslib: ^2.3.1 + checksum: 295450b417a9ab0b734050afff6c53aaed8a33dccd3ede60bf67fdec21f675d14ab8edc24f4e1d12aa4e99f9ccaf794aaaaff270c296c1ee38f73ea7ba7f59ce + languageName: node + linkType: hard + +"@aws-sdk/lib-storage@npm:^3.208.0": + version: 3.223.0 + resolution: "@aws-sdk/lib-storage@npm:3.223.0" + dependencies: + "@aws-sdk/middleware-endpoint": 3.222.0 + "@aws-sdk/smithy-client": 3.222.0 + buffer: 5.6.0 + events: 3.3.0 + stream-browserify: 3.0.0 + tslib: ^2.3.1 + peerDependencies: + "@aws-sdk/abort-controller": ^3.0.0 + "@aws-sdk/client-s3": ^3.0.0 + checksum: 85dccc0148b404cb822156eafca32e918835f2beb6f218fd6062cce3ea93f80cf7229dd58f2a1c06959880958cb893a2b3e6e05526e080cd76c9b3971689602f + languageName: node + linkType: hard + +"@aws-sdk/md5-js@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/md5-js@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-utf8-browser": 3.188.0 + "@aws-sdk/util-utf8-node": 3.208.0 + tslib: ^2.3.1 + checksum: 1fce491eef05bbf11242472e65a8ac379fd8061c3afc23e1e320fe86ddcb2babaa4696e1ebb452e69fcacf4f812d92490e25b7c928edb44d667db6718e1310cd + languageName: node + linkType: hard + +"@aws-sdk/middleware-bucket-endpoint@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.222.0" + dependencies: + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-arn-parser": 3.208.0 + "@aws-sdk/util-config-provider": 3.208.0 + tslib: ^2.3.1 + checksum: 2f1d0da868788b8716a39e79e885cad3fa481701f6965eade6acd9d60c39fa95a257ca3c3d70b84936cead854232c17ddd6d2d4ce414735959af567e518aeb6e + languageName: node + linkType: hard + +"@aws-sdk/middleware-content-length@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-content-length@npm:3.222.0" + dependencies: + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: c21d04fa94ad3b20f507ad41f65cd010e84ce6ef9a4b9ae9c8d66ea8ca60b8b4c1436a9a1c19805de79b95b6f885f18dfda50ba19f6546023a71f36577797f66 + languageName: node + linkType: hard + +"@aws-sdk/middleware-endpoint@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-endpoint@npm:3.222.0" + dependencies: + "@aws-sdk/middleware-serde": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/signature-v4": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/url-parser": 3.222.0 + "@aws-sdk/util-config-provider": 3.208.0 + "@aws-sdk/util-middleware": 3.222.0 + tslib: ^2.3.1 + checksum: 3a1240f66457d1de73a9c9f5f8baaf628f307d46840ef323492ac68ede5504dee6b1a9299195212e2b12e039ac7d6eafefa72cc1502799bcae6fd77c5d84da93 + languageName: node + linkType: hard + +"@aws-sdk/middleware-expect-continue@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.222.0" + dependencies: + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 285ed55ab00f9a46af98003a34af9398e2ab044f0892f51b4f498ec1bfc7b83065bf4d6680da9bc2a581fcab438eac76a385b1038e3683aec8c9056285ef9a2f + languageName: node + linkType: hard + +"@aws-sdk/middleware-flexible-checksums@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.222.0" + dependencies: + "@aws-crypto/crc32": 2.0.0 + "@aws-crypto/crc32c": 2.0.0 + "@aws-sdk/is-array-buffer": 3.201.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: b55f9f45b4fd3c9aeed2f80c030b19fb1e2602e1062c692b4f7c5c9d87e7b7c036e7ebdac066dbfe6a3cde275a9091235ff2793a657630a330aaa652c6e53b50 + languageName: node + linkType: hard + +"@aws-sdk/middleware-host-header@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.222.0" + dependencies: + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 0806ce2b235d8ec6328caa1cacdbdbee82196f82b7dc96e1ce6d8413390a35c73475df3a102391a99cd5996ef806054969ea00338064889997bd15428a064c73 + languageName: node + linkType: hard + +"@aws-sdk/middleware-location-constraint@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 54d77c68746e836ec9317f4062152beb09fccee15266f52b7e4ee6a19d5d40b8c4bb720a867170acdf8c77d8ef7b08701cf1a76b202dc3c191488a10507c1bdc + languageName: node + linkType: hard + +"@aws-sdk/middleware-logger@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-logger@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: c0fef1f56ba710a2e5784515f96d8c8408b3e882288d1bc8e6201bf8128d921de4b2dbaaeca1fee48334539587bf514cb77796cc0fab989173c4d785fc220db5 + languageName: node + linkType: hard + +"@aws-sdk/middleware-recursion-detection@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.222.0" + dependencies: + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 6a3b31de18a32585a86948f312aefd98ec7a9a9ff5b8044f7f11dc8d7737e5fdb5d867f2971ffcf645db118892a28518fdb9689cc8fb64e947717281cca32031 + languageName: node + linkType: hard + +"@aws-sdk/middleware-retry@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-retry@npm:3.222.0" + dependencies: + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/service-error-classification": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-middleware": 3.222.0 + tslib: ^2.3.1 + uuid: ^8.3.2 + checksum: 764a56b92ba0178166cb188db20e521360a625d763a94e9470eeda83ad1d6d240186b5eeec41592896aa09f7c1370e604513419bf30683cbcbd5e5ae625b3a98 + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-s3@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.222.0" + dependencies: + "@aws-sdk/middleware-bucket-endpoint": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-arn-parser": 3.208.0 + tslib: ^2.3.1 + checksum: a09aead012e6aae12467ba45b1740255b60da4f5cd816043abc8af0fd4905133063454c755d08ab3dcbf11096d42d0f92ab20b51d08c7d4daaa55f0586e99558 + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-sqs@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-hex-encoding": 3.201.0 + tslib: ^2.3.1 + checksum: b882f5e83fa65f8e2f1dceaa96a0995d2058fee84dbd16048198b9f45d1f6763215363fbdb99b8f137a580b8cdac820992e776a562378327d7d86b6cca4e8d87 + languageName: node + linkType: hard + +"@aws-sdk/middleware-sdk-sts@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-sdk-sts@npm:3.222.0" + dependencies: + "@aws-sdk/middleware-signing": 3.222.0 + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/signature-v4": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 0ad309d6490586686643602199b8a4389818568b6a7f6c41c97ff0b85e91651c9bed05da31c3eddf1ad329af926ac7efa5b8ee353c52a540ef1a529d6346350f + languageName: node + linkType: hard + +"@aws-sdk/middleware-serde@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-serde@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 28dfe6d9c8d662d449ec4df9c30aa0eb18103f8cff8f3fa5325fc41a4e94b16e054d97d1325fe68b2ef497abfeabfc96b4ad63e485fb71693a488869fe732c33 + languageName: node + linkType: hard + +"@aws-sdk/middleware-signing@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-signing@npm:3.222.0" + dependencies: + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/signature-v4": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-middleware": 3.222.0 + tslib: ^2.3.1 + checksum: 0e43183ba97d40162d2be2d8f0965c532691ebbb9bcb6216cb9b43f6fa2ed81118f3c06f991e5d01b3dc5b8d3cc692344b53996a8e13085fb9c658683da5e791 + languageName: node + linkType: hard + +"@aws-sdk/middleware-ssec@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-ssec@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: fec3cf9a4495c1d2bdcd1ca3d577a077df9427a8fb1afbf53eede63347e1d0181b031e7c4625736079a726bf9c90ae5140aed2655c2282b8dcfc2957ca2641af + languageName: node + linkType: hard + +"@aws-sdk/middleware-stack@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-stack@npm:3.222.0" + dependencies: + tslib: ^2.3.1 + checksum: 5f143c1a964016a7296798e8ca457b7a364bc8e816eb1829be5c3b5c0f7430dbad2df3725d96bee4f4ad9ac4937083e7b2bf7c72093775c5b9fa85f697304361 + languageName: node + linkType: hard + +"@aws-sdk/middleware-user-agent@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.222.0" + dependencies: + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 3521fc61597faa8e53ee2290a4e1342ad0a22591362757c57985add5e3a85978f9e8fc83e993670740eae532937886c61ca85a8c534327ff4ef1ed721883e7a9 + languageName: node + linkType: hard + +"@aws-sdk/node-config-provider@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/node-config-provider@npm:3.222.0" + dependencies: + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/shared-ini-file-loader": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 53c2e7d7c1ac5c54a0fbf5b5fdf963c89cc659f51189a6086f939d5adc0d9c3500e3205b159285f447df3bbf39ac32e67e1c313bbc368ad1c1ae8d26f771a3f4 + languageName: node + linkType: hard + +"@aws-sdk/node-http-handler@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/node-http-handler@npm:3.222.0" + dependencies: + "@aws-sdk/abort-controller": 3.222.0 + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/querystring-builder": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 06a66d1e54e0fd856c6172f2cfd97daefab8d5b996eecfb83b05cb29f4830ff00af1a5deb2726ec88b306a585be214e3fc7b3ead3ea9b70776a70a768424a70d + languageName: node + linkType: hard + +"@aws-sdk/property-provider@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/property-provider@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 924965232eeef1870c7ed108b7ab1b6d23da3032f0741b129a883eab472c17c9811b95f79e3e84299cd0a44512b3aedfafa05ab0b232a0b17782ed327a4a1965 + languageName: node + linkType: hard + +"@aws-sdk/protocol-http@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/protocol-http@npm:3.208.0" + dependencies: + "@aws-sdk/types": 3.208.0 + tslib: ^2.3.1 + checksum: 6cf57f264a4ef393fbe5fac71f22eaba47bc421646063dac004919ec7185979ed91a3e03c71373749150249f6be4ae41294a4c068d7c22a5990dcd2f5cf8347c + languageName: node + linkType: hard + +"@aws-sdk/protocol-http@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/protocol-http@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 11650667a4072246e6a12269c95e2c5a746d32032d0a54e3c8d062c711fef07b0c6fc2ad2a23c9ef82a97d305bf9a9222ccbcc3adb2835355b8b0199d3839f75 + languageName: node + linkType: hard + +"@aws-sdk/querystring-builder@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/querystring-builder@npm:3.208.0" + dependencies: + "@aws-sdk/types": 3.208.0 + "@aws-sdk/util-uri-escape": 3.201.0 + tslib: ^2.3.1 + checksum: 69ff07e2a05ae0979da4e5b12ffb223f0eb037fab079f1f4c245101fdb2d9244454a543dcc829120b882a99a63ecfc950f209ba5a1aaacf94432a35a214c2521 + languageName: node + linkType: hard + +"@aws-sdk/querystring-builder@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/querystring-builder@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-uri-escape": 3.201.0 + tslib: ^2.3.1 + checksum: 418bc648f3597e1b96765784acb6964dc3aaca6853582d7e91aaaaf793d9ee1fe8170da31cddff72b36b95456fc5566589120c94988711726dc80ddf345e9839 + languageName: node + linkType: hard + +"@aws-sdk/querystring-parser@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/querystring-parser@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: d209807dc727c6eebf885d1e7a440e0fdb9c68e27f7f6e48a326f97eab8da9172dba91a35015656dffd10d7a2b484237e49705d332948cfed747ae04f08de9ab + languageName: node + linkType: hard + +"@aws-sdk/service-error-classification@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/service-error-classification@npm:3.222.0" + checksum: 7277ef3831a91821ce3306eda993d4d646ae15de9c638a4a8804d8db330a267cb6e3a20e3eb886d9cac634a77b0e1f3eb0706ce7c38bc42f4b67a1fac6f6d76a + languageName: node + linkType: hard + +"@aws-sdk/shared-ini-file-loader@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/shared-ini-file-loader@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 2efc2618e65058bd549a6acd2cd68c1e8d2a5079a5447328908eb952651714d616fdb1a95622c63b4718f03ac251eb6d5598a666e1c01f1ea223b2071ee74371 + languageName: node + linkType: hard + +"@aws-sdk/signature-v4-multi-region@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.222.0" + dependencies: + "@aws-sdk/protocol-http": 3.222.0 + "@aws-sdk/signature-v4": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-arn-parser": 3.208.0 + tslib: ^2.3.1 + peerDependencies: + "@aws-sdk/signature-v4-crt": ^3.118.0 + peerDependenciesMeta: + "@aws-sdk/signature-v4-crt": + optional: true + checksum: 6ab68c8001c85c79974f5bd272e6caeebe486d40665be1658aba4bce11e518873fc3bc72c9731fbfa399fa5d09b980f878aaa1c01330ebc0b026520c8d3f3fed + languageName: node + linkType: hard + +"@aws-sdk/signature-v4@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/signature-v4@npm:3.222.0" + dependencies: + "@aws-sdk/is-array-buffer": 3.201.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-hex-encoding": 3.201.0 + "@aws-sdk/util-middleware": 3.222.0 + "@aws-sdk/util-uri-escape": 3.201.0 + tslib: ^2.3.1 + checksum: 66f9f0f8170cdede19d4194a5bb56fc914c9013bf9dbc60d84aa4340d15d4f5ed73a477484fac943de08f57d924423eaa01f913bd3055ee0b7b3c5b119f9e348 + languageName: node + linkType: hard + +"@aws-sdk/smithy-client@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/smithy-client@npm:3.222.0" + dependencies: + "@aws-sdk/middleware-stack": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 0bfbd061a001054a99234a819e9313a2bfd9131e013cdd46674f3dc06faa3e416245d8e3a86e05fad942da5ec6ef34988e686e29d5940ad738452116a4e70173 + languageName: node + linkType: hard + +"@aws-sdk/token-providers@npm:3.223.0": + version: 3.223.0 + resolution: "@aws-sdk/token-providers@npm:3.223.0" + dependencies: + "@aws-sdk/client-sso-oidc": 3.223.0 + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/shared-ini-file-loader": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 08d724bc685b90761159b449413c9dddb581cc61cd982ee9a189e8682d1a703acc70663beec2ae31d1ebf6fe9a86480b63fb65da531e35b2e8772a2c3906969f + languageName: node + linkType: hard + +"@aws-sdk/types@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/types@npm:3.208.0" + checksum: 3d4eb51720bd20b6811dece5e096ba3b59da842d1c8b2a8e2785e9931d916f41197651da689aa415083e1259e03507b7703ecbbac001b4cdbe68dde2ed94b5cf + languageName: node + linkType: hard + +"@aws-sdk/types@npm:3.222.0, @aws-sdk/types@npm:^3.1.0, @aws-sdk/types@npm:^3.110.0, @aws-sdk/types@npm:^3.208.0": + version: 3.222.0 + resolution: "@aws-sdk/types@npm:3.222.0" + checksum: 41d70038c69b1e9b9dbf0489a8aa416da1c96c4ff84e8d5db01ea4877efceb4c0fdd0ce27bdeea8c67afc4d168151dc63938302a3f945922143f37472c4fd9da + languageName: node + linkType: hard + +"@aws-sdk/url-parser@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/url-parser@npm:3.222.0" + dependencies: + "@aws-sdk/querystring-parser": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: ef878c49c61efc2bd6a94ffebfbacade2bdd19e390785e48a8c5c151db86d099c391c30304ba6ade1cdd94c1c41d87ce160d6570cd4f76058e1cbbe0063829f4 + languageName: node + linkType: hard + +"@aws-sdk/util-arn-parser@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/util-arn-parser@npm:3.208.0" + dependencies: + tslib: ^2.3.1 + checksum: 1e1793adc0c53801b9fd7ccb7e94169dcd984047533c28897aea2af4c4fa02cbc2cd1b5d9b6f34eaf6b9aaa0aba27f927c8e2a58a6b4bb06888cfa68d66b6b2d + languageName: node + linkType: hard + +"@aws-sdk/util-base64@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/util-base64@npm:3.208.0" + dependencies: + "@aws-sdk/util-buffer-from": 3.208.0 + tslib: ^2.3.1 + checksum: 2ccab3453a3a3636f3f1397441574b3adb984e1ba3865030393108327ed7304cf80c9b31d69691e6aba85cfe6a611a881bbb724e544324240763bb4e96630ed9 + languageName: node + linkType: hard + +"@aws-sdk/util-body-length-browser@npm:3.188.0": + version: 3.188.0 + resolution: "@aws-sdk/util-body-length-browser@npm:3.188.0" + dependencies: + tslib: ^2.3.1 + checksum: 1b08bd1e63ec843ee336f51d894c49bf3c4c2f96e50d1711a12f7d0c5b6f7a15b490e366fec55b63e77036002994bac12927b29de2eb9ac91e4f152b1af78e58 + languageName: node + linkType: hard + +"@aws-sdk/util-body-length-node@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/util-body-length-node@npm:3.208.0" + dependencies: + tslib: ^2.3.1 + checksum: 986b42b358656dec4e75c231213331c4f01785f9ab17c8b87b6e268b6880818a96117f1785cef9786e6c0f7e2c1332c80e8388a43bfd83e8c7224ad059a72733 + languageName: node + linkType: hard + +"@aws-sdk/util-buffer-from@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/util-buffer-from@npm:3.208.0" + dependencies: + "@aws-sdk/is-array-buffer": 3.201.0 + tslib: ^2.3.1 + checksum: 00bfa4d4494d3a1eb128e19104994d1aca8b3802e9aa218cecafb1ed3ff2ecf5c946485e06aa97ae312458842b0f31a6484dc945232f7cb0e357ba341cb2e53e + languageName: node + linkType: hard + +"@aws-sdk/util-config-provider@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/util-config-provider@npm:3.208.0" + dependencies: + tslib: ^2.3.1 + checksum: 97b0414b120b4eb53001f3ab2135ee94937e47bd7bd0d0de7c6a7e00a282eaa78cd84be2bfd3e389340f0c0b2f7ba60da9a403f084721970ee55b779ecf7a451 + languageName: node + linkType: hard + +"@aws-sdk/util-defaults-mode-browser@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/util-defaults-mode-browser@npm:3.222.0" + dependencies: + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/types": 3.222.0 + bowser: ^2.11.0 + tslib: ^2.3.1 + checksum: 3e936e38976a9edac22ff32990c55ca01a9f3bb058ebf1f933a1f5240bb6c26394224284db521b9011e88bdb6d567b7aa7584e0a594ae962ad9f0e3740801712 + languageName: node + linkType: hard + +"@aws-sdk/util-defaults-mode-node@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/util-defaults-mode-node@npm:3.222.0" + dependencies: + "@aws-sdk/config-resolver": 3.222.0 + "@aws-sdk/credential-provider-imds": 3.222.0 + "@aws-sdk/node-config-provider": 3.222.0 + "@aws-sdk/property-provider": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 37718633f793e9880f4a93e6d56cb72b1cfc4e45d3177d2065e5179788941cf0f43fb45c740763debbe33fc8a62d1d09c3fe87809c71c039a5dd071cec3a0306 + languageName: node + linkType: hard + +"@aws-sdk/util-endpoints@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/util-endpoints@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 3e71a50994363b978ab35bc1fee7ab21c157731e0c7364f064d40244058c96188c238b27d2f9097becc548b86e0abaca886cea03e89b8b219fb0d0a7fea6a249 + languageName: node + linkType: hard + +"@aws-sdk/util-hex-encoding@npm:3.201.0": + version: 3.201.0 + resolution: "@aws-sdk/util-hex-encoding@npm:3.201.0" + dependencies: + tslib: ^2.3.1 + checksum: a27f3365dfb1e6ece79ea34fd6e2c4540eb0084536d7300ff0ff42a7334ddf07f21958c6cfd0bbeb71361ee408e16deae2c82b7c7378b048b8e81a52c75f190a + languageName: node + linkType: hard + +"@aws-sdk/util-locate-window@npm:^3.0.0": + version: 3.183.0 + resolution: "@aws-sdk/util-locate-window@npm:3.183.0" + dependencies: + tslib: ^2.3.1 + checksum: c29899b440c148aa44a5d9322e06e4cf21644e47bee10555cf8db104d9d76b3985fa4da6e8ca92c137b0b739f7504a7d106e9da49e1d189b84f51f45c21b2204 + languageName: node + linkType: hard + +"@aws-sdk/util-middleware@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/util-middleware@npm:3.222.0" + dependencies: + tslib: ^2.3.1 + checksum: 807dd36731c7aa59658e4eea5747d6d363b5e5715d0d2c5554c265df6729735f31e790e8e6130d5d56b4923e29080a656e19978631a7ff667f969bfdae8c6320 + languageName: node + linkType: hard + +"@aws-sdk/util-stream-browser@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/util-stream-browser@npm:3.222.0" + dependencies: + "@aws-sdk/fetch-http-handler": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-base64": 3.208.0 + "@aws-sdk/util-hex-encoding": 3.201.0 + "@aws-sdk/util-utf8-browser": 3.188.0 + tslib: ^2.3.1 + checksum: 08428921f9a49824ea5b335c4bdacb6216346b8c0fd2e6914a45982c96d440eabc4a05a91e217ff7a5696d802c2fb1ab458546c34ef66edf5e52f41e3ee98dda + languageName: node + linkType: hard + +"@aws-sdk/util-stream-node@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/util-stream-node@npm:3.222.0" + dependencies: + "@aws-sdk/node-http-handler": 3.222.0 + "@aws-sdk/types": 3.222.0 + "@aws-sdk/util-buffer-from": 3.208.0 + tslib: ^2.3.1 + checksum: 04d875a572d45604dc71c1e20b90136f5b8a20820f53ad5ab4e76ee86fa3e043e015c40d041d81822703305c9530baccc595534f6674903b7f830ea9649c050b + languageName: node + linkType: hard + +"@aws-sdk/util-uri-escape@npm:3.201.0": + version: 3.201.0 + resolution: "@aws-sdk/util-uri-escape@npm:3.201.0" + dependencies: + "@aws-sdk/fetch-http-handler": 3.208.0 + "@aws-sdk/types": 3.208.0 + "@aws-sdk/util-base64": 3.208.0 + "@aws-sdk/util-hex-encoding": 3.201.0 + "@aws-sdk/util-utf8-browser": 3.188.0 + tslib: ^2.3.1 + checksum: 8bd751459eaab75a9b61801f3484cfa5c4e0133381ace6ec901cb9b92b1fee99beb4ef9c0f87ade59425a882ed3a280255d9b2fd8da6a6286e49efb9af8f0d55 + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-browser@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.222.0" + dependencies: + "@aws-sdk/types": 3.222.0 + bowser: ^2.11.0 + tslib: ^2.3.1 + checksum: 14a878c88948fed8c6e19324a6f28088cbb964580e74c97ec319c8afb426f17b35cfbcbc40bb43b977b3a5a9b05aaa254dfc23a028d29802058148f28507f7f1 + languageName: node + linkType: hard + +"@aws-sdk/util-user-agent-node@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.222.0" + dependencies: + "@aws-sdk/node-config-provider": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + peerDependencies: + aws-crt: ">=1.0.0" + peerDependenciesMeta: + aws-crt: + optional: true + checksum: 36a9cf397196ef6c188d307f6202b10d98629dfe7583dc903e22870b553cca7624e2732f16e9511e690433665966e5adce7472299081eda1acf8d33b4c977ea4 + languageName: node + linkType: hard + +"@aws-sdk/util-utf8-browser@npm:3.188.0, @aws-sdk/util-utf8-browser@npm:^3.0.0": + version: 3.188.0 + resolution: "@aws-sdk/util-utf8-browser@npm:3.188.0" + dependencies: + tslib: ^2.3.1 + checksum: dacd27164aa0835888434e080b67f04510e2281560540ff73496f2d0aa73b0b7f830ec08491b35c3a51bf6214615579182aff8727e151e54a74a97a197a2ac31 + languageName: node + linkType: hard + +"@aws-sdk/util-utf8-node@npm:3.208.0": + version: 3.208.0 + resolution: "@aws-sdk/util-utf8-node@npm:3.208.0" + dependencies: + "@aws-sdk/util-buffer-from": 3.208.0 + tslib: ^2.3.1 + checksum: 0f4c1d0d3f6eb40d92bc7163c6dcf97f90b3a8be05df46ba4f9bb7723a3e6acabf2cb0f2486411b03d22e8fcccae9e757ed02afad384ff5f44a3a659a78c5752 + languageName: node + linkType: hard + +"@aws-sdk/util-waiter@npm:3.222.0": + version: 3.222.0 + resolution: "@aws-sdk/util-waiter@npm:3.222.0" + dependencies: + "@aws-sdk/abort-controller": 3.222.0 + "@aws-sdk/types": 3.222.0 + tslib: ^2.3.1 + checksum: 840d57ff2250d6a9fd917ed80db5d8ecea38b056453aaab78c7b19c9858f6e837ffd06f430e5682d0fbd4f3d47ac1a79fcf9f1ed9af7da2e446e3a19239777ec + languageName: node + linkType: hard + +"@aws-sdk/xml-builder@npm:3.201.0": + version: 3.201.0 + resolution: "@aws-sdk/xml-builder@npm:3.201.0" + dependencies: + tslib: ^2.3.1 + checksum: fd250c2edcf062e79af576fa39d555f624c155540f7bb27da05a400540f3c2ceb89f33841157eda5f4558a0b6c58d2686a105d26f79eeab5790e6b6bfbe18d4a + languageName: node + linkType: hard + "@azure/abort-controller@npm:^1.0.0": version: 1.0.2 resolution: "@azure/abort-controller@npm:1.0.2" @@ -476,7 +1769,7 @@ __metadata: languageName: node linkType: hard -"@azure/identity@npm:^2.0.1, @azure/identity@npm:^2.0.4, @azure/identity@npm:^2.1.0": +"@azure/identity@npm:^2.0.4, @azure/identity@npm:^2.1.0": version: 2.1.0 resolution: "@azure/identity@npm:2.1.0" dependencies: @@ -2090,8 +3383,9 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/types": "workspace:^" "@google-cloud/storage": ^6.0.0 - "@keyv/redis": ^2.2.3 - "@kubernetes/client-node": ^0.17.0 + "@keyv/memcache": ^1.3.5 + "@keyv/redis": ^2.5.3 + "@kubernetes/client-node": 0.17.0 "@manypkg/get-packages": ^1.1.3 "@octokit/rest": ^19.0.3 "@types/archiver": ^5.1.0 @@ -2102,7 +3396,7 @@ __metadata: "@types/dockerode": ^3.3.0 "@types/express": ^4.17.6 "@types/fs-extra": ^9.0.3 - "@types/http-errors": ^1.6.3 + "@types/http-errors": ^2.0.0 "@types/luxon": ^3.0.0 "@types/minimist": ^1.2.0 "@types/mock-fs": ^4.13.0 @@ -2131,8 +3425,7 @@ __metadata: http-errors: ^2.0.0 isomorphic-git: ^1.8.0 jose: ^4.6.0 - keyv: ^4.0.3 - keyv-memcache: ^1.2.5 + keyv: ^4.5.2 knex: ^2.0.0 lodash: ^4.17.21 logform: ^2.3.2 @@ -2141,9 +3434,8 @@ __metadata: minimist: ^1.2.5 mock-fs: ^5.1.0 morgan: ^1.10.0 - msw: ^0.47.0 + msw: ^0.49.0 mysql2: ^2.2.5 - node-abort-controller: ^3.0.1 node-fetch: ^2.6.7 node-forge: ^1.3.1 raw-body: ^2.4.1 @@ -2152,7 +3444,7 @@ __metadata: selfsigned: ^2.0.0 stoppable: ^1.1.0 supertest: ^6.1.3 - tar: ^6.1.2 + tar: ^6.1.12 uuid: ^8.3.2 winston: ^3.2.1 yauzl: ^2.10.0 @@ -2207,7 +3499,6 @@ __metadata: knex: ^2.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - node-abort-controller: ^3.0.1 uuid: ^8.0.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 @@ -2226,7 +3517,7 @@ __metadata: "@backstage/config": "workspace:^" better-sqlite3: ^7.5.0 knex: ^2.0.0 - msw: ^0.47.0 + msw: ^0.49.0 mysql2: ^2.2.5 pg: ^8.3.0 testcontainers: ^8.1.2 @@ -2253,7 +3544,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/errors": "workspace:^" cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 languageName: unknown linkType: soft @@ -2319,10 +3610,10 @@ __metadata: "@manypkg/get-packages": ^1.1.3 "@octokit/request": ^6.0.0 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.7 - "@rollup/plugin-commonjs": ^22.0.0 - "@rollup/plugin-json": ^4.1.0 + "@rollup/plugin-commonjs": ^23.0.0 + "@rollup/plugin-json": ^5.0.0 "@rollup/plugin-node-resolve": ^13.0.6 - "@rollup/plugin-yaml": ^3.1.0 + "@rollup/plugin-yaml": ^4.0.0 "@spotify/eslint-config-base": ^14.0.0 "@spotify/eslint-config-react": ^14.0.0 "@spotify/eslint-config-typescript": ^14.0.0 @@ -2365,7 +3656,7 @@ __metadata: css-loader: ^6.5.1 del: ^6.0.0 diff: ^5.0.0 - esbuild: ^0.14.10 + esbuild: ^0.15.0 esbuild-loader: ^2.18.0 eslint: ^8.6.0 eslint-config-prettier: ^8.3.0 @@ -2393,9 +3684,9 @@ __metadata: json-schema: ^0.4.0 lodash: ^4.17.21 mini-css-extract-plugin: ^2.4.2 - minimatch: 5.1.0 + minimatch: ^5.1.1 mock-fs: ^5.1.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 node-libs-browser: ^2.2.1 nodemon: ^2.0.2 @@ -2417,7 +3708,7 @@ __metadata: style-loader: ^3.3.1 sucrase: ^3.20.2 swc-loader: ^0.2.3 - tar: ^6.1.2 + tar: ^6.1.12 terser-webpack-plugin: ^5.1.3 ts-node: ^10.0.0 type-fest: ^2.0.0 @@ -2449,7 +3740,7 @@ __metadata: "@types/node": ^16.11.26 chalk: ^4.0.0 commander: ^9.1.0 - jscodeshift: ^0.13.0 + jscodeshift: ^0.14.0 jscodeshift-add-imports: ^1.0.10 ts-node: ^10.0.0 bin: @@ -2478,9 +3769,9 @@ __metadata: json-schema-merge-allof: ^0.8.1 json-schema-traverse: ^1.0.0 mock-fs: ^5.1.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 - typescript-json-schema: ^0.54.0 + typescript-json-schema: ^0.55.0 yaml: ^2.0.0 yup: ^0.32.9 languageName: unknown @@ -2526,14 +3817,14 @@ __metadata: "@types/prop-types": ^15.7.3 "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 prop-types: ^15.7.2 react-router-beta: "npm:react-router@6.0.0-beta.0" react-router-dom-beta: "npm:react-router-dom@6.0.0-beta.0" react-router-dom-stable: "npm:react-router-dom@^6.3.0" react-router-stable: "npm:react-router@^6.3.0" react-use: ^17.2.4 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 zod: ^3.11.6 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 @@ -2660,7 +3951,7 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 - "@react-hookz/web": ^15.0.0 + "@react-hookz/web": ^20.0.0 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 @@ -2690,11 +3981,11 @@ __metadata: history: ^5.0.0 immer: ^9.0.1 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 pluralize: ^8.0.0 prop-types: ^15.7.2 qs: ^6.9.4 - rc-progress: 3.4.0 + rc-progress: 3.4.1 react-helmet: 6.1.0 react-hook-form: ^7.12.2 react-markdown: ^8.0.0 @@ -2705,7 +3996,7 @@ __metadata: react-virtualized-auto-sizer: ^1.0.6 react-window: ^1.8.6 remark-gfm: ^3.0.1 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 zod: ^3.11.6 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 @@ -2753,9 +4044,9 @@ __metadata: "@types/zen-observable": ^0.8.0 cross-fetch: ^3.1.5 history: ^5.0.0 - msw: ^0.47.0 + msw: ^0.49.0 prop-types: ^15.7.2 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 react: ^16.13.1 || ^17.0.0 @@ -2810,7 +4101,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.0.0 react-use: ^17.2.4 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 react: ^16.13.1 || ^17.0.0 @@ -2881,7 +4172,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -2920,7 +4211,7 @@ __metadata: git-url-parse: ^13.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 languageName: unknown linkType: soft @@ -2941,7 +4232,7 @@ __metadata: "@types/supertest": ^2.0.8 luxon: ^3.0.0 marked: ^4.0.14 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.5 supertest: ^6.1.3 winston: ^3.2.1 @@ -2987,7 +4278,7 @@ __metadata: "@types/node": "*" cross-fetch: ^3.1.5 git-url-parse: ^13.0.0 - msw: ^0.47.0 + msw: ^0.49.0 octokit: ^2.0.0 react-markdown: ^8.0.0 react-use: ^17.2.4 @@ -3011,7 +4302,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 http-proxy-middleware: ^2.0.0 - msw: ^0.47.0 + msw: ^0.49.0 supertest: ^6.1.6 winston: ^3.2.1 yn: ^4.0.0 @@ -3040,7 +4331,7 @@ __metadata: "@types/node": ^16.11.26 "@types/object-hash": ^2.2.1 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 object-hash: ^3.0.0 react-use: ^17.2.4 peerDependencies: @@ -3070,7 +4361,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -3098,7 +4389,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-ga: ^3.3.0 react-use: ^17.2.4 peerDependencies: @@ -3124,7 +4415,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 qs: ^6.10.1 react-use: ^17.2.4 peerDependencies: @@ -3149,7 +4440,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-api-docs@workspace:plugins/api-docs" dependencies: - "@asyncapi/react-component": 1.0.0-next.43 + "@asyncapi/react-component": 1.0.0-next.44 "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" @@ -3173,7 +4464,7 @@ __metadata: graphql: ^16.0.0 graphql-ws: ^5.4.1 isomorphic-form-data: ^2.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 swagger-ui-react: ^4.11.1 peerDependencies: @@ -3204,7 +4495,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": "*" cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 use-deep-compare-effect: ^1.8.1 peerDependencies: @@ -3236,7 +4527,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 mock-fs: ^5.1.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 supertest: ^6.1.3 winston: ^3.2.1 @@ -3287,10 +4578,10 @@ __metadata: luxon: ^3.0.0 minimatch: ^5.0.0 morgan: ^1.10.0 - msw: ^0.47.0 + msw: ^0.49.0 node-cache: ^5.1.2 node-fetch: ^2.6.7 - openid-client: ^5.1.3 + openid-client: ^5.2.1 passport: ^0.6.0 passport-auth0: ^1.4.3 passport-bitbucket-oauth2: ^0.1.2 @@ -3321,7 +4612,7 @@ __metadata: express: ^4.17.1 jose: ^4.6.0 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -3342,7 +4633,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 mime-types: ^2.1.27 - msw: ^0.47.0 + msw: ^0.49.0 p-limit: ^3.1.0 supertest: ^6.1.6 winston: ^3.2.1 @@ -3383,7 +4674,7 @@ __metadata: cross-fetch: ^3.1.5 humanize-duration: ^3.27.0 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -3406,7 +4697,7 @@ __metadata: "@types/supertest": ^2.0.8 express: ^4.17.1 express-promise-router: ^4.1.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 winston: ^3.2.1 yn: ^4.0.0 @@ -3444,7 +4735,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -3495,7 +4786,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -3559,7 +4850,7 @@ __metadata: "@backstage/integration": "workspace:^" "@openapitools/openapi-generator-cli": ^2.4.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 ts-morph: ^15.0.0 languageName: unknown linkType: soft @@ -3588,7 +4879,7 @@ __metadata: cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 qs: ^6.9.6 react-use: ^17.2.4 recharts: ^2.0.0 @@ -3644,7 +4935,7 @@ __metadata: "@types/lodash": ^4.14.151 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -3659,14 +4950,18 @@ __metadata: "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-bitbucket-cloud-common": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 uuid: ^8.0.0 winston: ^3.2.1 languageName: unknown @@ -3689,7 +4984,7 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" "@types/node-fetch": ^2.5.12 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -3712,7 +5007,7 @@ __metadata: "@backstage/types": "workspace:^" "@types/lodash": ^4.14.151 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 winston: ^3.2.1 languageName: unknown @@ -3736,7 +5031,7 @@ __metadata: "@types/fs-extra": ^9.0.1 fs-extra: 10.1.0 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -3758,7 +5053,9 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" "@backstage/types": "workspace:^" "@octokit/graphql": ^5.0.0 "@octokit/rest": ^19.0.3 @@ -3766,7 +5063,7 @@ __metadata: git-url-parse: ^13.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 @@ -3793,13 +5090,42 @@ __metadata: "@types/uuid": ^8.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 uuid: ^8.0.0 winston: ^3.2.1 languageName: unknown linkType: soft +"@backstage/plugin-catalog-backend-module-incremental-ingestion@workspace:plugins/catalog-backend-module-incremental-ingestion": + version: 0.0.0-use.local + resolution: "@backstage/plugin-catalog-backend-module-incremental-ingestion@workspace:plugins/catalog-backend-module-incremental-ingestion" + dependencies: + "@backstage/backend-app-api": "workspace:^" + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-backend": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" + "@backstage/plugin-permission-common": "workspace:^" + "@types/express": ^4.17.6 + "@types/luxon": ^3.0.0 + express: ^4.17.1 + express-promise-router: ^4.1.0 + get-port: ^6.1.2 + knex: ^2.0.0 + lodash: ^4.17.21 + luxon: ^3.0.0 + uuid: ^8.3.2 + winston: ^3.2.1 + languageName: unknown + linkType: soft + "@backstage/plugin-catalog-backend-module-ldap@workspace:plugins/catalog-backend-module-ldap": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-ldap@workspace:plugins/catalog-backend-module-ldap" @@ -3839,7 +5165,7 @@ __metadata: "@types/node-fetch": ^2.5.12 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 p-limit: ^3.0.2 qs: ^6.9.4 @@ -3852,7 +5178,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-openapi@workspace:plugins/catalog-backend-module-openapi" dependencies: - "@apidevtools/swagger-parser": ^10.1.0 + "@apidevtools/json-schema-ref-parser": ^9.0.6 "@backstage/backend-common": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^" @@ -3907,7 +5233,7 @@ __metadata: knex: ^2.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 p-limit: ^3.0.2 prom-client: ^14.0.1 @@ -3997,7 +5323,7 @@ __metadata: graphql-modules: ^2.0.0 graphql-tag: ^2.11.0 graphql-type-json: ^0.3.2 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 winston: ^3.2.1 languageName: unknown @@ -4033,7 +5359,7 @@ __metadata: git-url-parse: ^13.0.0 js-base64: ^3.6.0 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 react-hook-form: ^7.12.2 react-use: ^17.2.4 yaml: ^2.0.0 @@ -4131,7 +5457,7 @@ __metadata: react-test-renderer: ^16.13.1 react-use: ^17.2.4 yaml: ^2.0.0 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 react: ^16.13.1 || ^17.0.0 @@ -4169,9 +5495,10 @@ __metadata: cross-fetch: ^3.1.5 history: ^5.0.0 lodash: ^4.17.21 + pluralize: ^8.0.0 react-helmet: 6.1.0 react-use: ^17.2.4 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 react: ^16.13.1 || ^17.0.0 @@ -4246,7 +5573,7 @@ __metadata: humanize-duration: ^3.27.0 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4277,7 +5604,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 qs: ^6.9.4 react-use: ^17.2.4 peerDependencies: @@ -4311,7 +5638,7 @@ __metadata: cross-fetch: ^3.1.5 humanize-duration: ^3.27.1 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4337,7 +5664,7 @@ __metadata: express-promise-router: ^4.1.0 express-xml-bodyparser: ^0.3.0 knex: ^2.0.0 - msw: ^0.47.0 + msw: ^0.49.0 supertest: ^6.1.6 uuid: ^8.3.2 winston: ^3.2.1 @@ -4374,7 +5701,7 @@ __metadata: cross-fetch: ^3.1.5 highlight.js: ^10.6.0 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 recharts: ^2.0.0 peerDependencies: @@ -4405,8 +5732,8 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": "*" cross-fetch: ^3.1.5 - msw: ^0.47.0 - rc-progress: 3.4.0 + msw: ^0.49.0 + rc-progress: 3.4.1 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4437,9 +5764,9 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 jsonschema: ^1.2.6 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 peerDependencies: react: ^16.13.1 || ^17.0.0 languageName: unknown @@ -4464,6 +5791,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-cost-insights-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" @@ -4479,12 +5807,12 @@ __metadata: "@types/recharts": ^1.8.14 "@types/regression": ^2.0.0 "@types/yup": ^0.29.13 - canvas: ^2.6.1 + canvas: ^2.10.2 classnames: ^2.2.6 cross-fetch: ^3.1.5 history: ^5.0.0 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 pluralize: ^8.0.0 qs: ^6.9.4 react-use: ^17.2.4 @@ -4518,7 +5846,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": "*" express: ^4.18.1 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: "@backstage/plugin-catalog-react": "workspace:^" @@ -4526,6 +5854,167 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-events-backend-module-aws-sqs@workspace:plugins/events-backend-module-aws-sqs": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-backend-module-aws-sqs@workspace:plugins/events-backend-module-aws-sqs" + dependencies: + "@aws-sdk/client-sqs": ^3.208.0 + "@aws-sdk/types": ^3.208.0 + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + "@backstage/types": "workspace:^" + aws-sdk-client-mock: ^2.0.0 + luxon: ^3.0.0 + winston: ^3.2.1 + languageName: unknown + linkType: soft + +"@backstage/plugin-events-backend-module-azure@workspace:plugins/events-backend-module-azure": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-backend-module-azure@workspace:plugins/events-backend-module-azure" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + supertest: ^6.1.3 + winston: ^3.2.1 + languageName: unknown + linkType: soft + +"@backstage/plugin-events-backend-module-bitbucket-cloud@workspace:plugins/events-backend-module-bitbucket-cloud": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-backend-module-bitbucket-cloud@workspace:plugins/events-backend-module-bitbucket-cloud" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + supertest: ^6.1.3 + winston: ^3.2.1 + languageName: unknown + linkType: soft + +"@backstage/plugin-events-backend-module-gerrit@workspace:plugins/events-backend-module-gerrit": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-backend-module-gerrit@workspace:plugins/events-backend-module-gerrit" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + supertest: ^6.1.3 + winston: ^3.2.1 + languageName: unknown + linkType: soft + +"@backstage/plugin-events-backend-module-github@workspace:plugins/events-backend-module-github": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-backend-module-github@workspace:plugins/events-backend-module-github" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + "@octokit/webhooks-methods": ^3.0.0 + supertest: ^6.1.3 + winston: ^3.2.1 + languageName: unknown + linkType: soft + +"@backstage/plugin-events-backend-module-gitlab@workspace:plugins/events-backend-module-gitlab": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-backend-module-gitlab@workspace:plugins/events-backend-module-gitlab" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + supertest: ^6.1.3 + winston: ^3.2.1 + languageName: unknown + linkType: soft + +"@backstage/plugin-events-backend-test-utils@workspace:^, @backstage/plugin-events-backend-test-utils@workspace:plugins/events-backend-test-utils": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-backend-test-utils@workspace:plugins/events-backend-test-utils" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + languageName: unknown + linkType: soft + +"@backstage/plugin-events-backend@workspace:^, @backstage/plugin-events-backend@workspace:plugins/events-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-backend@workspace:plugins/events-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + "@types/express": ^4.17.6 + express: ^4.17.1 + express-promise-router: ^4.1.0 + supertest: ^6.1.3 + winston: ^3.2.1 + languageName: unknown + linkType: soft + +"@backstage/plugin-events-node@workspace:^, @backstage/plugin-events-node@workspace:plugins/events-node": + version: 0.0.0-use.local + resolution: "@backstage/plugin-events-node@workspace:plugins/events-node" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + +"@backstage/plugin-explore-backend@workspace:^, @backstage/plugin-explore-backend@workspace:plugins/explore-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-explore-backend@workspace:plugins/explore-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-explore-common": "workspace:^" + "@backstage/plugin-search-common": "workspace:^" + "@types/express": "*" + "@types/supertest": ^2.0.8 + express: ^4.18.1 + express-promise-router: ^4.1.0 + lodash: ^4.17.21 + msw: ^0.47.0 + node-fetch: ^2.6.7 + supertest: ^6.2.4 + winston: ^3.2.1 + yn: ^4.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-explore-common@workspace:^, @backstage/plugin-explore-common@workspace:plugins/explore-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-explore-common@workspace:plugins/explore-common" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/plugin-explore-react@workspace:^, @backstage/plugin-explore-react@workspace:plugins/explore-react": version: 0.0.0-use.local resolution: "@backstage/plugin-explore-react@workspace:plugins/explore-react" @@ -4533,13 +6022,14 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/plugin-explore-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 languageName: unknown linkType: soft @@ -4553,9 +6043,13 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-explore-common": "workspace:^" "@backstage/plugin-explore-react": "workspace:^" + "@backstage/plugin-search-common": "workspace:^" + "@backstage/plugin-search-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@material-ui/core": ^4.12.2 @@ -4567,7 +6061,7 @@ __metadata: "@types/node": ^16.11.26 classnames: ^2.2.6 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 @@ -4598,7 +6092,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4628,7 +6122,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 p-limit: ^3.0.2 react-use: ^17.2.4 peerDependencies: @@ -4665,7 +6159,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 material-ui-popup-state: ^1.9.3 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4686,13 +6180,13 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 - "@react-hookz/web": ^15.0.0 + "@react-hookz/web": ^20.0.0 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 peerDependencies: react: ^16.13.1 || ^17.0.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 @@ -4723,7 +6217,7 @@ __metadata: "@types/recharts": ^1.8.15 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 qs: ^6.10.1 react-use: ^17.2.4 recharts: ^2.0.0 @@ -4758,7 +6252,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4793,7 +6287,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4826,7 +6320,7 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 octokit: ^2.0.4 react-use: ^17.4.0 peerDependencies: @@ -4858,7 +6352,7 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 p-limit: ^4.0.0 react-use: ^17.2.4 peerDependencies: @@ -4886,7 +6380,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4920,7 +6414,7 @@ __metadata: cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 qs: ^6.10.1 react-use: ^17.2.4 peerDependencies: @@ -4951,7 +6445,7 @@ __metadata: graphiql: ^1.5.12 graphql: ^16.0.0 graphql-ws: ^5.4.1 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -4977,7 +6471,7 @@ __metadata: graphql: ^16.0.0 graphql-modules: ^2.0.0 helmet: ^6.0.0 - msw: ^0.47.0 + msw: ^0.49.0 reflect-metadata: ^0.1.13 supertest: ^6.1.3 winston: ^3.2.1 @@ -5033,7 +6527,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 @@ -5068,7 +6562,7 @@ __metadata: cross-fetch: ^3.1.5 humanize-duration: ^3.26.0 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5094,8 +6588,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 jenkins: ^1.0.0 - msw: ^0.47.0 - promise-any-polyfill: ^1.0.1 + msw: ^0.49.0 supertest: ^6.1.6 winston: ^3.2.1 yn: ^4.0.0 @@ -5137,7 +6630,7 @@ __metadata: "@types/testing-library__jest-dom": ^5.9.1 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5192,7 +6685,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 jest-when: ^3.1.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5206,6 +6699,7 @@ __metadata: dependencies: "@azure/identity": ^2.0.4 "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" @@ -5214,9 +6708,11 @@ __metadata: "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@google-cloud/container": ^4.0.0 - "@kubernetes/client-node": ^0.17.0 + "@jest-mock/express": ^2.0.1 + "@kubernetes/client-node": 0.17.0 "@types/aws4": ^1.5.1 "@types/express": ^4.17.6 + "@types/http-proxy-middleware": ^0.19.3 "@types/luxon": ^3.0.0 aws-sdk: ^2.840.0 aws-sdk-mock: ^5.2.1 @@ -5227,9 +6723,12 @@ __metadata: express-promise-router: ^4.1.0 fs-extra: 10.1.0 helmet: ^6.0.0 + http-proxy-middleware: ^2.0.6 lodash: ^4.17.21 luxon: ^3.0.0 morgan: ^1.10.0 + msw: ^0.49.0 + node-fetch: ^2.6.7 stream-buffers: ^3.0.2 supertest: ^6.1.3 winston: ^3.2.1 @@ -5243,7 +6742,7 @@ __metadata: dependencies: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" - "@kubernetes/client-node": ^0.17.0 + "@kubernetes/client-node": 0.17.0 languageName: unknown linkType: soft @@ -5262,7 +6761,7 @@ __metadata: "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" - "@kubernetes/client-node": ^0.17.0 + "@kubernetes/client-node": 0.17.0 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 @@ -5277,7 +6776,7 @@ __metadata: js-yaml: ^4.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5309,7 +6808,7 @@ __metadata: "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5359,7 +6858,36 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-org-react@workspace:plugins/org-react": + version: 0.0.0-use.local + resolution: "@backstage/plugin-org-react@workspace:plugins/org-react" + dependencies: + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5388,7 +6916,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 p-limit: ^3.1.0 pluralize: ^8.0.0 qs: ^6.10.1 @@ -5424,7 +6952,7 @@ __metadata: classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 @@ -5444,7 +6972,7 @@ __metadata: "@types/supertest": ^2.0.8 express: ^4.17.1 express-promise-router: ^4.1.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 supertest: ^6.1.6 winston: ^3.2.1 @@ -5476,7 +7004,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -5502,7 +7030,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 supertest: ^6.1.6 winston: ^3.2.1 @@ -5534,7 +7062,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/types": "workspace:^" cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 uuid: ^8.0.0 zod: ^3.11.6 languageName: unknown @@ -5555,7 +7083,7 @@ __metadata: "@types/supertest": ^2.0.8 express: ^4.17.1 express-promise-router: ^4.1.0 - msw: ^0.47.0 + msw: ^0.49.0 supertest: ^6.1.3 zod: ^3.11.6 zod-to-json-schema: ^3.18.1 @@ -5621,7 +7149,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 knex: ^2.0.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 supertest: ^6.1.3 uuid: ^8.2.0 @@ -5669,7 +7197,7 @@ __metadata: "@types/node": "*" cross-fetch: ^3.1.5 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 qs: ^6.9.4 react-hook-form: ^7.13.0 react-use: ^17.2.4 @@ -5697,7 +7225,7 @@ __metadata: express-promise-router: ^4.1.0 http-proxy-middleware: ^2.0.0 morgan: ^1.10.0 - msw: ^0.47.0 + msw: ^0.49.0 supertest: ^6.1.3 uuid: ^8.0.0 winston: ^3.2.1 @@ -5725,7 +7253,7 @@ __metadata: fs-extra: 10.1.0 lodash: ^4.17.21 morgan: ^1.10.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 supertest: ^6.1.3 winston: ^3.2.1 @@ -5757,7 +7285,7 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 react-sparklines: ^1.7.0 react-use: ^17.2.4 peerDependencies: @@ -5784,7 +7312,7 @@ __metadata: command-exists: ^1.2.9 fs-extra: 10.1.0 mock-fs: ^5.1.0 - msw: ^0.47.0 + msw: ^0.49.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -5860,7 +7388,7 @@ __metadata: command-exists: ^1.2.9 compression: ^1.7.4 cors: ^2.8.5 - esbuild: ^0.14.1 + esbuild: ^0.15.0 express: ^4.17.1 express-promise-router: ^4.1.0 fs-extra: 10.1.0 @@ -5875,7 +7403,7 @@ __metadata: luxon: ^3.0.0 mock-fs: ^5.1.0 morgan: ^1.10.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 nunjucks: ^3.2.3 octokit: ^2.0.0 @@ -5888,7 +7416,7 @@ __metadata: vm2: ^3.9.11 winston: ^3.2.1 yaml: ^2.0.0 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 zod: ^3.11.6 languageName: unknown linkType: soft @@ -5932,13 +7460,13 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 - "@react-hookz/web": ^15.0.0 + "@react-hookz/web": ^20.0.0 "@rjsf/core": ^3.2.1 - "@rjsf/core-v5": "npm:@rjsf/core@^5.0.0-beta.10" + "@rjsf/core-v5": "npm:@rjsf/core@^5.0.0-beta.12" "@rjsf/material-ui": ^3.2.1 - "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@^5.0.0-beta.10" - "@rjsf/utils": ^5.0.0-beta.10 - "@rjsf/validator-ajv8": ^5.0.0-beta.10 + "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@^5.0.0-beta.12" + "@rjsf/utils": ^5.0.0-beta.12 + "@rjsf/validator-ajv8": ^5.0.0-beta.12 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 @@ -5954,15 +7482,17 @@ __metadata: humanize-duration: ^3.25.1 immer: ^9.0.1 json-schema: ^0.4.0 - json-schema-library: ^7.0.0 + json-schema-library: ^7.3.9 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 qs: ^6.9.4 react-use: ^17.2.4 use-immer: ^0.7.0 yaml: ^2.0.0 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 + zod: ^3.11.6 + zod-to-json-schema: ^3.18.1 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 react: ^16.13.1 || ^17.0.0 @@ -6025,7 +7555,6 @@ __metadata: lodash: ^4.17.21 lunr: ^2.3.9 ndjson: ^2.0.0 - node-abort-controller: ^3.0.1 uuid: ^8.3.2 winston: ^3.2.1 languageName: unknown @@ -6137,7 +7666,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 qs: ^6.9.4 react-use: ^17.2.4 peerDependencies: @@ -6173,7 +7702,7 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-sparklines: ^1.7.0 react-use: ^17.2.4 peerDependencies: @@ -6203,11 +7732,11 @@ __metadata: "@types/node": ^16.11.26 "@types/zen-observable": ^0.8.2 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-hook-form: ^7.12.2 react-use: ^17.2.4 uuid: ^8.3.2 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 peerDependencies: react: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 @@ -6227,7 +7756,7 @@ __metadata: "@types/supertest": ^2.0.12 express: ^4.18.1 express-promise-router: ^4.1.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 supertest: ^6.2.4 winston: ^3.2.1 @@ -6257,8 +7786,8 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 - rc-progress: 3.4.0 + msw: ^0.49.0 + rc-progress: 3.4.1 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6289,7 +7818,7 @@ __metadata: classnames: ^2.2.6 cross-fetch: ^3.1.5 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6356,7 +7885,7 @@ __metadata: "@types/node": ^16.11.26 cross-fetch: ^3.1.5 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 qs: ^6.9.4 react-use: ^17.2.4 peerDependencies: @@ -6467,7 +7996,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 qs: ^6.9.4 react-use: ^17.2.4 peerDependencies: @@ -6501,7 +8030,7 @@ __metadata: color: ^4.0.1 cross-fetch: ^3.1.5 d3-force: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 prop-types: ^15.7.2 react-use: ^17.2.4 peerDependencies: @@ -6533,7 +8062,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 testing-library__dom: ^7.29.4-beta.1 peerDependencies: @@ -6569,7 +8098,7 @@ __metadata: fs-extra: 10.1.0 knex: ^2.0.0 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 p-limit: ^3.1.0 supertest: ^6.1.3 @@ -6595,7 +8124,7 @@ __metadata: "@material-ui/core": ^4.9.13 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 - "@react-hookz/web": ^15.0.0 + "@react-hookz/web": ^20.0.0 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 @@ -6603,7 +8132,7 @@ __metadata: "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 git-url-parse: ^13.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6614,7 +8143,11 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-techdocs-node@workspace:plugins/techdocs-node" dependencies: - "@azure/identity": ^2.0.1 + "@aws-sdk/client-s3": ^3.208.0 + "@aws-sdk/credential-providers": ^3.208.0 + "@aws-sdk/lib-storage": ^3.208.0 + "@aws-sdk/types": ^3.208.0 + "@azure/identity": ^2.1.0 "@azure/storage-blob": ^12.5.0 "@backstage/backend-common": "workspace:^" "@backstage/catalog-model": "workspace:^" @@ -6632,7 +8165,7 @@ __metadata: "@types/mock-fs": ^4.13.0 "@types/recursive-readdir": ^2.2.0 "@types/supertest": ^2.0.8 - aws-sdk: ^2.840.0 + aws-sdk-client-mock: ^2.0.0 express: ^4.17.1 fs-extra: 10.1.0 git-url-parse: ^13.0.0 @@ -6707,14 +8240,14 @@ __metadata: "@types/dompurify": ^2.2.2 "@types/event-source-polyfill": ^1.0.0 "@types/node": ^16.11.26 - canvas: ^2.6.1 + canvas: ^2.10.2 cross-fetch: ^3.1.5 dompurify: ^2.2.9 event-source-polyfill: 1.0.25 git-url-parse: ^13.0.0 jss: ~10.8.2 lodash: ^4.17.21 - msw: ^0.47.0 + msw: ^0.49.0 react-helmet: 6.1.0 react-use: ^17.2.4 peerDependencies: @@ -6742,7 +8275,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 leasot: ^12.0.0 - msw: ^0.47.0 + msw: ^0.49.0 supertest: ^6.1.3 winston: ^3.2.1 yn: ^4.0.0 @@ -6771,7 +8304,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6823,9 +8356,9 @@ __metadata: "@types/node": ^16.11.26 "@types/react": ^16.13.1 || ^17.0.0 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 peerDependencies: react: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 @@ -6850,7 +8383,7 @@ __metadata: express: ^4.17.1 express-promise-router: ^4.1.0 helmet: ^6.0.0 - msw: ^0.47.0 + msw: ^0.49.0 node-fetch: ^2.6.7 p-limit: ^3.1.0 supertest: ^6.1.6 @@ -6881,7 +8414,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": "*" cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -6911,7 +8444,7 @@ __metadata: cross-fetch: ^3.1.5 lodash: ^4.17.21 luxon: ^3.0.0 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 recharts: ^2.0.0 peerDependencies: @@ -6927,7 +8460,26 @@ __metadata: "@backstage/test-utils": "workspace:^" "@types/node": ^16.0.0 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 + languageName: unknown + linkType: soft + +"@backstage/repo-tools@workspace:*, @backstage/repo-tools@workspace:packages/repo-tools": + version: 0.0.0-use.local + resolution: "@backstage/repo-tools@workspace:packages/repo-tools" + dependencies: + "@backstage/errors": "workspace:^" + "@manypkg/get-packages": ^1.1.3 + "@microsoft/api-documenter": ^7.17.11 + "@microsoft/api-extractor": ^7.23.0 + "@microsoft/api-extractor-model": ^7.17.2 + "@microsoft/tsdoc": 0.14.1 + chalk: ^4.0.0 + commander: ^9.1.0 + fs-extra: 10.1.0 + ts-node: ^10.0.0 + bin: + backstage-repo-tools: bin/backstage-repo-tools languageName: unknown linkType: soft @@ -6950,8 +8502,8 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 - zen-observable: ^0.8.15 + msw: ^0.49.0 + zen-observable: ^0.10.0 peerDependencies: "@types/react": ^16.13.1 || ^17.0.0 react: ^16.13.1 || ^17.0.0 @@ -6984,11 +8536,21 @@ __metadata: "@backstage/cli": "workspace:^" "@types/zen-observable": ^0.8.0 luxon: ^3.0.0 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 languageName: unknown linkType: soft -"@backstage/version-bridge@^1.0.1, @backstage/version-bridge@workspace:^, @backstage/version-bridge@workspace:packages/version-bridge": +"@backstage/version-bridge@npm:^1.0.1, @backstage/version-bridge@npm:^1.0.2": + version: 1.0.2 + resolution: "@backstage/version-bridge@npm:1.0.2" + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + checksum: b45cbf5340afb7896d6baa2a650ee21e9a63743ee589d07a30139a6e035e7e5f996a5b9fa08f5db2ccdd95db288c4b4e73b17e419f7569e061065fd362f38e75 + languageName: node + linkType: hard + +"@backstage/version-bridge@workspace:^, @backstage/version-bridge@workspace:packages/version-bridge": version: 0.0.0-use.local resolution: "@backstage/version-bridge@workspace:packages/version-bridge" dependencies: @@ -7002,16 +8564,6 @@ __metadata: languageName: unknown linkType: soft -"@backstage/version-bridge@npm:^1.0.2": - version: 1.0.2 - resolution: "@backstage/version-bridge@npm:1.0.2" - peerDependencies: - "@types/react": ^16.13.1 || ^17.0.0 - react: ^16.13.1 || ^17.0.0 - checksum: b45cbf5340afb7896d6baa2a650ee21e9a63743ee589d07a30139a6e035e7e5f996a5b9fa08f5db2ccdd95db288c4b4e73b17e419f7569e061065fd362f38e75 - languageName: node - linkType: hard - "@balena/dockerignore@npm:^1.0.2": version: 1.0.2 resolution: "@balena/dockerignore@npm:1.0.2" @@ -7550,13 +9102,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.14.54": - version: 0.14.54 - resolution: "@esbuild/linux-loong64@npm:0.14.54" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.15.18": version: 0.15.18 resolution: "@esbuild/linux-loong64@npm:0.15.18" @@ -8468,7 +10013,7 @@ __metadata: "@types/uuid": ^8.0.0 express: ^4.17.1 express-promise-router: ^4.1.0 - msw: ^0.47.0 + msw: ^0.49.0 supertest: ^6.1.6 uuid: ^8.3.2 winston: ^3.2.1 @@ -8487,7 +10032,7 @@ __metadata: "@backstage/test-utils": "workspace:^" "@types/node": ^16.11.26 cross-fetch: ^3.1.5 - msw: ^0.47.0 + msw: ^0.49.0 languageName: unknown linkType: soft @@ -8509,7 +10054,7 @@ __metadata: "@testing-library/react": ^12.1.3 "@testing-library/user-event": ^14.0.0 "@types/node": ^16.11.26 - msw: ^0.47.0 + msw: ^0.49.0 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -8549,6 +10094,15 @@ __metadata: languageName: node linkType: hard +"@jest-mock/express@npm:^2.0.1": + version: 2.0.1 + resolution: "@jest-mock/express@npm:2.0.1" + dependencies: + "@types/express": ^4.17.13 + checksum: 999ea0a953b3e911d0b8ecc4cb3b78ac252b3354832db9659be6754094410b10508f1688f3e623ab13fe40c28d89fd5a81699e6acd148dd8128abec4e81f3f14 + languageName: node + linkType: hard + "@jest/console@npm:^29.3.1": version: 29.3.1 resolution: "@jest/console@npm:29.3.1" @@ -9338,19 +10892,36 @@ __metadata: languageName: node linkType: hard -"@keyv/redis@npm:^2.2.3": - version: 2.5.4 - resolution: "@keyv/redis@npm:2.5.4" +"@keyv/memcache@npm:^1.3.5": + version: 1.3.5 + resolution: "@keyv/memcache@npm:1.3.5" dependencies: - ioredis: ^5.2.4 - checksum: 02f557e0bc0343aea44b8dd4b6ff273da5833b935236c4036fa89e90b1d92d9688dafa064d5318d26744a6c72497cd4c41dcac825cb8d5919271e3a25a3cadbb + json-buffer: ^3.0.1 + memjs: ^1.3.0 + checksum: c457863aeff1c5886c273eafa5fd79a4287cae1d9d3721b6ce5aa699033428ec8930db044224a6ab63d951cdac2408850737d0adc65f0a502bf04cd7dbe0d15e languageName: node linkType: hard -"@kubernetes/client-node@npm:^0.17.0": - version: 0.17.1 - resolution: "@kubernetes/client-node@npm:0.17.1" +"@keyv/redis@npm:^2.5.3": + version: 2.5.3 + resolution: "@keyv/redis@npm:2.5.3" dependencies: + ioredis: ^5.2.4 + checksum: db6be54624320f5d03856985fee5151601fa278d63ef98feac12f89188fbf86b972e7f5ca3ece0d6137c81038b1eb3c42805beaf51c4c963cca35c2b7d68be92 + languageName: node + linkType: hard + +"@kubernetes/client-node@npm:0.17.0": + version: 0.17.0 + resolution: "@kubernetes/client-node@npm:0.17.0" + dependencies: + "@types/js-yaml": ^4.0.1 + "@types/node": ^10.12.0 + "@types/request": ^2.47.1 + "@types/stream-buffers": ^3.0.3 + "@types/tar": ^4.0.3 + "@types/underscore": ^1.8.9 + "@types/ws": ^6.0.1 byline: ^5.0.0 execa: 5.0.0 isomorphic-ws: ^4.0.1 @@ -9366,10 +10937,7 @@ __metadata: tslib: ^1.9.3 underscore: ^1.9.1 ws: ^7.3.1 - dependenciesMeta: - openid-client: - optional: true - checksum: 834ab0ca1f8583b06c4102395ad712c115a6f99201f8dd7f0b59c1a0a5da67b006d1fe5857ad909d762b9cf82a75503665df19fdb640bf620f6123ddb36c8872 + checksum: 5a6edce96946966d8b61359ab043b42e69d1b31178133bc3bf4b5a5c9191766986e08f85d6ae814b56546ed20389817a79dca5101342543d79d750f30cba21f7 languageName: node linkType: hard @@ -9774,13 +11342,6 @@ __metadata: languageName: node linkType: hard -"@microsoft/tsdoc@npm:^0.14.1": - version: 0.14.2 - resolution: "@microsoft/tsdoc@npm:0.14.2" - checksum: b167c89e916ba73ee20b9c9d5dba6aa3a0de25ed3d50050e8a344dca7cd43cb2e1059bd515c820369b6e708901dd3fda476a42bc643ca74a35671ce77f724a3a - languageName: node - linkType: hard - "@mswjs/cookies@npm:^0.2.0, @mswjs/cookies@npm:^0.2.2": version: 0.2.2 resolution: "@mswjs/cookies@npm:0.2.2" @@ -10856,6 +12417,22 @@ __metadata: languageName: node linkType: hard +"@react-hookz/web@npm:^20.0.0": + version: 20.0.0 + resolution: "@react-hookz/web@npm:20.0.0" + dependencies: + "@react-hookz/deep-equal": ^1.0.3 + peerDependencies: + js-cookie: ^3.0.1 + react: ^16.8 || ^17 || ^18 + react-dom: ^16.8 || ^17 || ^18 + peerDependenciesMeta: + js-cookie: + optional: true + checksum: 475d03cdd9a9131b7094549602c8a3ab52ad33e99d7ef408ba2ee7897bfdf332ab5228f46187b9a4c42535b4aaaf808e2c9c882949b2ec32d5ad42d53487a723 + languageName: node + linkType: hard + "@remix-run/router@npm:1.0.4": version: 1.0.4 resolution: "@remix-run/router@npm:1.0.4" @@ -10863,9 +12440,9 @@ __metadata: languageName: node linkType: hard -"@rjsf/core-v5@npm:@rjsf/core@^5.0.0-beta.10": - version: 5.0.0-beta.13 - resolution: "@rjsf/core@npm:5.0.0-beta.13" +"@rjsf/core-v5@npm:@rjsf/core@^5.0.0-beta.12": + version: 5.0.0-beta.12 + resolution: "@rjsf/core@npm:5.0.0-beta.12" dependencies: lodash: ^4.17.15 lodash-es: ^4.17.15 @@ -10874,7 +12451,7 @@ __metadata: peerDependencies: "@rjsf/utils": ^5.0.0-beta.1 react: ^16.14.0 || >=17 - checksum: dc57b9bc652ee115af0a3f9830bf6d394cab149169fdeac6a85232f41545f17d0bdbc2143a6a85c27f8f2d0b25e33a94c1d774c50402ba49faded08b9f4aaf1b + checksum: 6c3fe058a29716cb8d31d436e284aa7ab51ec3c272dc34d49275ea0033c3350a2486249767d454bca20a4ccd00ed910f7ed518d2ebe30693b0218db1b3c990ac languageName: node linkType: hard @@ -10897,16 +12474,16 @@ __metadata: languageName: node linkType: hard -"@rjsf/material-ui-v5@npm:@rjsf/material-ui@^5.0.0-beta.10": - version: 5.0.0-beta.13 - resolution: "@rjsf/material-ui@npm:5.0.0-beta.13" +"@rjsf/material-ui-v5@npm:@rjsf/material-ui@^5.0.0-beta.12": + version: 5.0.0-beta.12 + resolution: "@rjsf/material-ui@npm:5.0.0-beta.12" peerDependencies: "@material-ui/core": ^4.12.3 "@material-ui/icons": ^4.11.2 "@rjsf/core": ^5.0.0-beta.1 "@rjsf/utils": ^5.0.0-beta.1 react: ^16.14.0 || >=17 - checksum: c6e728376c0784f576305c1dfdd016a725986a8cd6924d72c763294c56a7817c9a26ecb51c21d5249034ecac39357abbb30b8527f2cb70025ba1e3224b6e0958 + checksum: 9b67c64956e74af6230d722fe9510fc50e6133c6913642f5bcbcb8da38b7ff5d912bc90a6b6692eb3df315ba9b330136556fb41854fb351e432433d584c1a969 languageName: node linkType: hard @@ -10922,9 +12499,9 @@ __metadata: languageName: node linkType: hard -"@rjsf/utils@npm:^5.0.0-beta.10": - version: 5.0.0-beta.13 - resolution: "@rjsf/utils@npm:5.0.0-beta.13" +"@rjsf/utils@npm:^5.0.0-beta.12": + version: 5.0.0-beta.12 + resolution: "@rjsf/utils@npm:5.0.0-beta.12" dependencies: json-schema-merge-allof: ^0.8.1 jsonpointer: ^5.0.1 @@ -10933,11 +12510,11 @@ __metadata: react-is: ^18.2.0 peerDependencies: react: ^16.14.0 || >=17 - checksum: 21400cc1f2f6c6020bef6f42663806f9d8d89d96edbb7dc058ae2b40af269c90a4644808903dd36dd700ca6a30185f38bbafe4bd97018671c9e44804ffc57e70 + checksum: b9698a2238dd5d3c79c13b84d92587825005938cd449680204903463b6fc6f63165fb715ea1e50a94e6764aa405be2b2ac2e6aad8bc68cd50623f065fd6f979a languageName: node linkType: hard -"@rjsf/validator-ajv8@npm:^5.0.0-beta.10": +"@rjsf/validator-ajv8@npm:^5.0.0-beta.12": version: 5.0.0-beta.13 resolution: "@rjsf/validator-ajv8@npm:5.0.0-beta.13" dependencies: @@ -11058,31 +12635,36 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-commonjs@npm:^22.0.0": - version: 22.0.2 - resolution: "@rollup/plugin-commonjs@npm:22.0.2" +"@rollup/plugin-commonjs@npm:^23.0.0": + version: 23.0.3 + resolution: "@rollup/plugin-commonjs@npm:23.0.3" dependencies: - "@rollup/pluginutils": ^3.1.0 + "@rollup/pluginutils": ^5.0.1 commondir: ^1.0.1 - estree-walker: ^2.0.1 - glob: ^7.1.6 - is-reference: ^1.2.1 - magic-string: ^0.25.7 - resolve: ^1.17.0 + estree-walker: ^2.0.2 + glob: ^8.0.3 + is-reference: 1.2.1 + magic-string: ^0.26.4 peerDependencies: - rollup: ^2.68.0 - checksum: 70098a4b91afe3f164f5d27cba65edf148c5ed146ee0e07a964b66940681553ac77391083114cdcf9427e7f2706bf0d61eab310b3a2caeab83b7452c0292fcae + rollup: ^2.68.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 925289c1694e871065d741ce210621df894febc267b9ca574dfe24e00a48b436f3eaa6a6f6d00312a78c9b51cc0cad7abf13b97fc455395576007c681d7ec58d languageName: node linkType: hard -"@rollup/plugin-json@npm:^4.1.0": - version: 4.1.0 - resolution: "@rollup/plugin-json@npm:4.1.0" +"@rollup/plugin-json@npm:^5.0.0": + version: 5.0.2 + resolution: "@rollup/plugin-json@npm:5.0.2" dependencies: - "@rollup/pluginutils": ^3.0.8 + "@rollup/pluginutils": ^5.0.1 peerDependencies: - rollup: ^1.20.0 || ^2.0.0 - checksum: 867bc9339b4ccf0b9ff3b2617a95b3b8920115163f86c8e3b1f068a14ca25949472d3c05b09a5ac38ca0fe2185756e34617eaeb219d4a2b6e2307c501c7d4552 + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 9b5f90ea311dfcfacf0f38af39bbb1954ea56d6faecdee3d528f73d0e02da96a0706ab21fae0c8eef9bb5d756f6f50b40b5a252ffd9800397012b5bac6764b6f languageName: node linkType: hard @@ -11102,20 +12684,23 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-yaml@npm:^3.1.0": - version: 3.1.0 - resolution: "@rollup/plugin-yaml@npm:3.1.0" +"@rollup/plugin-yaml@npm:^4.0.0": + version: 4.0.1 + resolution: "@rollup/plugin-yaml@npm:4.0.1" dependencies: - "@rollup/pluginutils": ^3.1.0 - js-yaml: ^3.14.0 - tosource: ^1.0.0 + "@rollup/pluginutils": ^5.0.1 + js-yaml: ^4.1.0 + tosource: ^2.0.0-alpha.3 peerDependencies: - rollup: ^1.20.0 || ^2.0.0 - checksum: be99aa097c480a24e6bedf85de2f8e21e287baf443155e7f2e765c939dda53f63cda674476f170b0397e3c69ce3bc825f0ad4041ac6fe25912480c27423e1b37 + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: d48e248a9acd3110c889d745562e03e5a16e5861838c6b6d92701e59c0d4a24af374ee7bd81db0ab8e6d021cdb925c05bd7d30149e4e9a0fce4d899170a78857 languageName: node linkType: hard -"@rollup/pluginutils@npm:^3.0.8, @rollup/pluginutils@npm:^3.1.0": +"@rollup/pluginutils@npm:^3.1.0": version: 3.1.0 resolution: "@rollup/pluginutils@npm:3.1.0" dependencies: @@ -11138,6 +12723,22 @@ __metadata: languageName: node linkType: hard +"@rollup/pluginutils@npm:^5.0.1": + version: 5.0.2 + resolution: "@rollup/pluginutils@npm:5.0.2" + dependencies: + "@types/estree": ^1.0.0 + estree-walker: ^2.0.2 + picomatch: ^2.3.1 + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: edea15e543bebc7dcac3b0ac8bc7b8e8e6dbd46e2864dbe5dd28072de1fbd5b0e10d545a610c0edaa178e8a7ac432e2a2a52e547ece1308471412caba47db8ce + languageName: node + linkType: hard + "@rushstack/node-core-library@npm:3.45.4": version: 3.45.4 resolution: "@rushstack/node-core-library@npm:3.45.4" @@ -11289,6 +12890,17 @@ __metadata: languageName: node linkType: hard +"@sinonjs/samsam@npm:^7.0.1": + version: 7.0.1 + resolution: "@sinonjs/samsam@npm:7.0.1" + dependencies: + "@sinonjs/commons": ^2.0.0 + lodash.get: ^4.4.2 + type-detect: ^4.0.8 + checksum: 291efb158d54c67dee23ddabcb28873d22063449b692aaa3b2a4f1826d2f79d38695574063c92e9c17573cc805cd6acbf0ab0c66c9f3aed7afd0f12a2b905615 + languageName: node + linkType: hard + "@sinonjs/text-encoding@npm:^0.7.1": version: 0.7.1 resolution: "@sinonjs/text-encoding@npm:0.7.1" @@ -12047,6 +13659,13 @@ __metadata: languageName: node linkType: hard +"@types/caseless@npm:*": + version: 0.12.2 + resolution: "@types/caseless@npm:0.12.2" + checksum: 430d15911184ad11e0a8aa21d1ec15fcc93b90b63570c37bf16ebd34457482bfc8de3f5eb6771e0ef986ce183270d4297823b0f492c346255967e78f7292388b + languageName: node + linkType: hard + "@types/classnames@npm:^2.2.9": version: 2.3.1 resolution: "@types/classnames@npm:2.3.1" @@ -12382,7 +14001,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*": +"@types/estree@npm:*, @types/estree@npm:^1.0.0": version: 1.0.0 resolution: "@types/estree@npm:1.0.0" checksum: 910d97fb7092c6738d30a7430ae4786a38542023c6302b95d46f49420b797f21619cdde11fa92b338366268795884111c2eb10356e4bd2c8ad5b92941e9e6443 @@ -12550,10 +14169,10 @@ __metadata: languageName: node linkType: hard -"@types/http-errors@npm:^1.6.3": - version: 1.8.2 - resolution: "@types/http-errors@npm:1.8.2" - checksum: ecc365eea98d7eca650d593e742571acc3003742f0dd0fbbb15b8fce286e0f7421644b4140fb9bf701bbb7f1b744aea3967ebe025f0f0811aa5ab2c3d40fe111 +"@types/http-errors@npm:^2.0.0": + version: 2.0.1 + resolution: "@types/http-errors@npm:2.0.1" + checksum: 3bb0c50b0a652e679a84c30cd0340d696c32ef6558518268c238840346c077f899315daaf1c26c09c57ddd5dc80510f2a7f46acd52bf949e339e35ed3ee9654f languageName: node linkType: hard @@ -12678,7 +14297,7 @@ __metadata: languageName: node linkType: hard -"@types/js-yaml@npm:^4.0.0": +"@types/js-yaml@npm:^4.0.0, @types/js-yaml@npm:^4.0.1": version: 4.0.5 resolution: "@types/js-yaml@npm:4.0.5" checksum: 7dcac8c50fec31643cc9d6444b5503239a861414cdfaa7ae9a38bc22597c4d850c4b8cec3d82d73b3fbca408348ce223b0408d598b32e094470dfffc6d486b4d @@ -12882,6 +14501,15 @@ __metadata: languageName: node linkType: hard +"@types/minipass@npm:*": + version: 3.1.2 + resolution: "@types/minipass@npm:3.1.2" + dependencies: + "@types/node": "*" + checksum: 0d01e11b5b959625385a482ad29ea16352be42506b459555b0f77fd82235e9c540946cc9c05a73fed1ae30b132914baaa4ccf257ed2cad20bc9773f0a06f4bac + languageName: node + linkType: hard + "@types/mock-fs@npm:^4.10.0, @types/mock-fs@npm:^4.13.0": version: 4.13.1 resolution: "@types/mock-fs@npm:4.13.1" @@ -12950,7 +14578,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^10.1.0": +"@types/node@npm:^10.1.0, @types/node@npm:^10.12.0": version: 10.17.60 resolution: "@types/node@npm:10.17.60" checksum: 2cdb3a77d071ba8513e5e8306fa64bf50e3c3302390feeaeff1fd325dd25c8441369715dfc8e3701011a72fed5958c7dfa94eb9239a81b3c286caa4d97db6eef @@ -13275,6 +14903,18 @@ __metadata: languageName: node linkType: hard +"@types/request@npm:^2.47.1": + version: 2.48.8 + resolution: "@types/request@npm:2.48.8" + dependencies: + "@types/caseless": "*" + "@types/node": "*" + "@types/tough-cookie": "*" + form-data: ^2.5.0 + checksum: 0b7754941e08205dce51635d894ec524df276d2b83ca13b9aab723f9281acecf1108841e9554494cb1cb60f6d6ddbb47ebea97392bcf2bf607f035b3a9b4af45 + languageName: node + linkType: hard + "@types/resize-observer-browser@npm:^0.1.6": version: 0.1.7 resolution: "@types/resize-observer-browser@npm:0.1.7" @@ -13393,6 +15033,22 @@ __metadata: languageName: node linkType: hard +"@types/sinon@npm:^10.0.10": + version: 10.0.13 + resolution: "@types/sinon@npm:10.0.13" + dependencies: + "@types/sinonjs__fake-timers": "*" + checksum: 46a14c888db50f0098ec53d451877e0111d878ec4a653b9e9ed7f8e54de386d6beb0e528ddc3e95cd3361a8ab9ad54e4cca33cd88d45b9227b83e9fc8fb6688a + languageName: node + linkType: hard + +"@types/sinonjs__fake-timers@npm:*": + version: 8.1.2 + resolution: "@types/sinonjs__fake-timers@npm:8.1.2" + checksum: bbc73a5ab6c0ec974929392f3d6e1e8db4ebad97ec506d785301e1c3d8a4f98a35b1aa95b97035daef02886fd8efd7788a2fa3ced2ec7105988bfd8dce61eedd + languageName: node + linkType: hard + "@types/sinonjs__fake-timers@npm:8.1.1": version: 8.1.1 resolution: "@types/sinonjs__fake-timers@npm:8.1.1" @@ -13451,6 +15107,15 @@ __metadata: languageName: node linkType: hard +"@types/stream-buffers@npm:^3.0.3": + version: 3.0.4 + resolution: "@types/stream-buffers@npm:3.0.4" + dependencies: + "@types/node": "*" + checksum: 5b432b2bf963d612747b79ac317562888236d6a9ea14414fb055c24e7be9643b5e3c7b7470841fa82802aa1c1c0d752a4ba935bbc0cfb12de6b89f7e1dadee92 + languageName: node + linkType: hard + "@types/styled-jsx@npm:^2.2.8": version: 2.2.8 resolution: "@types/styled-jsx@npm:2.2.8" @@ -13497,6 +15162,16 @@ __metadata: languageName: node linkType: hard +"@types/tar@npm:^4.0.3": + version: 4.0.5 + resolution: "@types/tar@npm:4.0.5" + dependencies: + "@types/minipass": "*" + "@types/node": "*" + checksum: 476d8af8f4cffcd973de026e043271be76171f9cb07bb869ba38a193ce89ee361b59ff8484e28b57216266063d91502c5208d2ab6b976e7370d9bdf4c4dadadc + languageName: node + linkType: hard + "@types/tar@npm:^6.1.1": version: 6.1.3 resolution: "@types/tar@npm:6.1.3" @@ -13566,6 +15241,13 @@ __metadata: languageName: node linkType: hard +"@types/underscore@npm:^1.8.9": + version: 1.11.4 + resolution: "@types/underscore@npm:1.11.4" + checksum: db9f8486bc851b732259e51f42d62aad1ae2158be5724612dc125ece5f5d61c51447f9dea28284c2a0f79cb95e788d01cb5ce97709880019213e69fab0dd1696 + languageName: node + linkType: hard + "@types/unist@npm:*, @types/unist@npm:^2.0.0": version: 2.0.6 resolution: "@types/unist@npm:2.0.6" @@ -13615,6 +15297,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^6.0.1": + version: 6.0.4 + resolution: "@types/ws@npm:6.0.4" + dependencies: + "@types/node": "*" + checksum: b2656a76bfad0c17bb1e3fc237ba7122431c1373669977ed8edef45934c82f71c75d8c71f0a576dc6d98b0954fd94cae0166c6b4ccb40f7e0ee29cc92673519c + languageName: node + linkType: hard + "@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.1": version: 8.5.3 resolution: "@types/ws@npm:8.5.3" @@ -13831,7 +15522,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:5.45.0, @typescript-eslint/utils@npm:^5.10.0": +"@typescript-eslint/utils@npm:5.45.0, @typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.13.0": version: 5.45.0 resolution: "@typescript-eslint/utils@npm:5.45.0" dependencies: @@ -14181,7 +15872,7 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:1": +"abbrev@npm:1, abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 @@ -14318,18 +16009,6 @@ __metadata: languageName: node linkType: hard -"ajv-draft-04@npm:^1.0.0": - version: 1.0.0 - resolution: "ajv-draft-04@npm:1.0.0" - peerDependencies: - ajv: ^8.5.0 - peerDependenciesMeta: - ajv: - optional: true - checksum: 3f11fa0e7f7359bef6608657f02ab78e9cc62b1fb7bdd860db0d00351b3863a1189c1a23b72466d2d82726cab4eb20725c76f5e7c134a89865e2bfd0e6828137 - languageName: node - linkType: hard - "ajv-formats@npm:^2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" @@ -14364,7 +16043,7 @@ __metadata: languageName: node linkType: hard -"ajv8@npm:ajv@^8.11.0, ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.6.3, ajv@npm:^8.8.0": +"ajv8@npm:ajv@^8.11.0, ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.8.0": version: 8.11.2 resolution: "ajv@npm:8.11.2" dependencies: @@ -14787,27 +16466,6 @@ __metadata: languageName: node linkType: hard -"arr-diff@npm:^4.0.0": - version: 4.0.0 - resolution: "arr-diff@npm:4.0.0" - checksum: ea7c8834842ad3869297f7915689bef3494fd5b102ac678c13ffccab672d3d1f35802b79e90c4cfec2f424af3392e44112d1ccf65da34562ed75e049597276a0 - languageName: node - linkType: hard - -"arr-flatten@npm:^1.1.0": - version: 1.1.0 - resolution: "arr-flatten@npm:1.1.0" - checksum: 963fe12564fca2f72c055f3f6c206b9e031f7c433a0c66ca9858b484821f248c5b1e5d53c8e4989d80d764cd776cf6d9b160ad05f47bdc63022bfd63b5455e22 - languageName: node - linkType: hard - -"arr-union@npm:^3.1.0": - version: 3.1.0 - resolution: "arr-union@npm:3.1.0" - checksum: b5b0408c6eb7591143c394f3be082fee690ddd21f0fdde0a0a01106799e847f67fcae1b7e56b0a0c173290e29c6aca9562e82b300708a268bc8f88f3d6613cb9 - languageName: node - linkType: hard - "array-differ@npm:^3.0.0": version: 3.0.0 resolution: "array-differ@npm:3.0.0" @@ -14872,13 +16530,6 @@ __metadata: languageName: node linkType: hard -"array-unique@npm:^0.3.2": - version: 0.3.2 - resolution: "array-unique@npm:0.3.2" - checksum: da344b89cfa6b0a5c221f965c21638bfb76b57b45184a01135382186924f55973cd9b171d4dad6bf606c6d9d36b0d721d091afdc9791535ead97ccbe78f8a888 - languageName: node - linkType: hard - "array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.2.5": version: 1.3.0 resolution: "array.prototype.flat@npm:1.3.0" @@ -14992,13 +16643,6 @@ __metadata: languageName: node linkType: hard -"assign-symbols@npm:^1.0.0": - version: 1.0.0 - resolution: "assign-symbols@npm:1.0.0" - checksum: c0eb895911d05b6b2d245154f70461c5e42c107457972e5ebba38d48967870dee53bcdf6c7047990586daa80fab8dab3cc6300800fbd47b454247fdedd859a2c - languageName: node - linkType: hard - "ast-types-flow@npm:^0.0.7": version: 0.0.7 resolution: "ast-types-flow@npm:0.0.7" @@ -15015,6 +16659,15 @@ __metadata: languageName: node linkType: hard +"ast-types@npm:0.15.2": + version: 0.15.2 + resolution: "ast-types@npm:0.15.2" + dependencies: + tslib: ^2.0.1 + checksum: 24f0d86bf9e4c8dae16fa24b13c1776f2c2677040bcfbd4eb4f27911db49020be4876885e45e6cfcc548ed4dfea3a0742d77e3346b84fae47379cb0b89e9daa0 + languageName: node + linkType: hard + "astral-regex@npm:^2.0.0": version: 2.0.0 resolution: "astral-regex@npm:2.0.0" @@ -15059,15 +16712,6 @@ __metadata: languageName: node linkType: hard -"atob@npm:^2.1.2": - version: 2.1.2 - resolution: "atob@npm:2.1.2" - bin: - atob: bin/atob.js - checksum: dfeeeb70090c5ebea7be4b9f787f866686c645d9f39a0d184c817252d0cf08455ed25267d79c03254d3be1f03ac399992a792edcd5ffb9c91e097ab5ef42833a - languageName: node - linkType: hard - "atomic-sleep@npm:^1.0.0": version: 1.0.0 resolution: "atomic-sleep@npm:1.0.0" @@ -15126,6 +16770,17 @@ __metadata: languageName: node linkType: hard +"aws-sdk-client-mock@npm:^2.0.0": + version: 2.0.1 + resolution: "aws-sdk-client-mock@npm:2.0.1" + dependencies: + "@types/sinon": ^10.0.10 + sinon: ^14.0.2 + tslib: ^2.1.0 + checksum: 447502ba9270777db129ddcf0245502771c1415d4293f7b001e0a3dc18ab0ce778a7f2d40a86dadb2637d3b83be2df4e833d7f4f6c49678495c932340558c465 + languageName: node + linkType: hard + "aws-sdk-mock@npm:^5.2.1": version: 5.7.0 resolution: "aws-sdk-mock@npm:5.7.0" @@ -15529,21 +17184,6 @@ __metadata: languageName: node linkType: hard -"base@npm:^0.11.1": - version: 0.11.2 - resolution: "base@npm:0.11.2" - dependencies: - cache-base: ^1.0.1 - class-utils: ^0.3.5 - component-emitter: ^1.2.1 - define-property: ^1.0.0 - isobject: ^3.0.1 - mixin-deep: ^1.2.0 - pascalcase: ^0.1.1 - checksum: a4a146b912e27eea8f66d09cb0c9eab666f32ce27859a7dfd50f38cd069a2557b39f16dba1bc2aecb3b44bf096738dd207b7970d99b0318423285ab1b1994edd - languageName: node - linkType: hard - "basic-auth@npm:~2.0.1": version: 2.0.1 resolution: "basic-auth@npm:2.0.1" @@ -15779,6 +17419,13 @@ __metadata: languageName: node linkType: hard +"bowser@npm:^2.11.0": + version: 2.11.0 + resolution: "bowser@npm:2.11.0" + checksum: 29c3f01f22e703fa6644fc3b684307442df4240b6e10f6cfe1b61c6ca5721073189ca97cdeedb376081148c8518e33b1d818a57f781d70b0b70e1f31fb48814f + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -15798,24 +17445,6 @@ __metadata: languageName: node linkType: hard -"braces@npm:^2.3.1": - version: 2.3.2 - resolution: "braces@npm:2.3.2" - dependencies: - arr-flatten: ^1.1.0 - array-unique: ^0.3.2 - extend-shallow: ^2.0.1 - fill-range: ^4.0.0 - isobject: ^3.0.1 - repeat-element: ^1.1.2 - snapdragon: ^0.8.1 - snapdragon-node: ^2.0.1 - split-string: ^3.0.2 - to-regex: ^3.0.1 - checksum: e30dcb6aaf4a31c8df17d848aa283a65699782f75ad61ae93ec25c9729c66cf58e66f0000a9fec84e4add1135bb7da40f7cb9601b36bebcfa9ca58e8d5c07de0 - languageName: node - linkType: hard - "braces@npm:^3.0.2, braces@npm:~3.0.2": version: 3.0.2 resolution: "braces@npm:3.0.2" @@ -16009,6 +17638,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:5.6.0": + version: 5.6.0 + resolution: "buffer@npm:5.6.0" + dependencies: + base64-js: ^1.0.2 + ieee754: ^1.1.4 + checksum: d659494c5032dd39d03d2912e64179cc44c6340e7e9d1f68d3840e7ab4559989fbce92b4950174593c38d05268224235ba404f0878775cab2a616b6dcad9c23e + languageName: node + linkType: hard + "buffer@npm:^5.2.0, buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.6.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" @@ -16132,23 +17771,6 @@ __metadata: languageName: node linkType: hard -"cache-base@npm:^1.0.1": - version: 1.0.1 - resolution: "cache-base@npm:1.0.1" - dependencies: - collection-visit: ^1.0.0 - component-emitter: ^1.2.1 - get-value: ^2.0.6 - has-value: ^1.0.0 - isobject: ^3.0.1 - set-value: ^2.0.0 - to-object-path: ^0.3.0 - union-value: ^1.0.0 - unset-value: ^1.0.0 - checksum: 9114b8654fe2366eedc390bad0bcf534e2f01b239a888894e2928cb58cdc1e6ea23a73c6f3450dcfd2058aa73a8a981e723cd1e7c670c047bf11afdc65880107 - languageName: node - linkType: hard - "cacheable-lookup@npm:^5.0.3": version: 5.0.3 resolution: "cacheable-lookup@npm:5.0.3" @@ -16289,7 +17911,7 @@ __metadata: languageName: node linkType: hard -"canvas@npm:^2.6.1": +"canvas@npm:^2.10.2": version: 2.10.2 resolution: "canvas@npm:2.10.2" dependencies: @@ -16575,18 +18197,6 @@ __metadata: languageName: node linkType: hard -"class-utils@npm:^0.3.5": - version: 0.3.6 - resolution: "class-utils@npm:0.3.6" - dependencies: - arr-union: ^3.1.0 - define-property: ^0.2.5 - isobject: ^3.0.0 - static-extend: ^0.1.1 - checksum: be108900801e639e50f96a7e4bfa8867c753a7750a7603879f3981f8b0a89cba657497a2d5f40cd4ea557ff15d535a100818bb486baf6e26fe5d7872e75f1078 - languageName: node - linkType: hard - "classnames@npm:*, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.1": version: 2.3.1 resolution: "classnames@npm:2.3.1" @@ -16881,16 +18491,6 @@ __metadata: languageName: node linkType: hard -"collection-visit@npm:^1.0.0": - version: 1.0.0 - resolution: "collection-visit@npm:1.0.0" - dependencies: - map-visit: ^1.0.0 - object-visit: ^1.0.0 - checksum: 15d9658fe6eb23594728346adad5433b86bb7a04fd51bbab337755158722f9313a5376ef479de5b35fbc54140764d0d39de89c339f5d25b959ed221466981da9 - languageName: node - linkType: hard - "color-convert@npm:^0.5.2": version: 0.5.3 resolution: "color-convert@npm:0.5.3" @@ -17149,7 +18749,7 @@ __metadata: languageName: node linkType: hard -"component-emitter@npm:^1.2.1, component-emitter@npm:^1.3.0, component-emitter@npm:~1.3.0": +"component-emitter@npm:^1.3.0, component-emitter@npm:~1.3.0": version: 1.3.0 resolution: "component-emitter@npm:1.3.0" checksum: b3c46de38ffd35c57d1c02488355be9f218e582aec72d72d1b8bbec95a3ac1b38c96cd6e03ff015577e68f550fbb361a3bfdbd9bb248be9390b7b3745691be6b @@ -17427,13 +19027,6 @@ __metadata: languageName: node linkType: hard -"copy-descriptor@npm:^0.1.0": - version: 0.1.1 - resolution: "copy-descriptor@npm:0.1.1" - checksum: d4b7b57b14f1d256bb9aa0b479241048afd7f5bcf22035fc7b94e8af757adeae247ea23c1a774fe44869fd5694efba4a969b88d966766c5245fdee59837fe45b - languageName: node - linkType: hard - "copy-to-clipboard@npm:^3.2.0, copy-to-clipboard@npm:^3.3.1": version: 3.3.1 resolution: "copy-to-clipboard@npm:3.3.1" @@ -18371,7 +19964,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:2.6.9, debug@npm:^2.2.0, debug@npm:^2.3.3, debug@npm:^2.6.0, debug@npm:^2.6.9": +"debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" dependencies: @@ -18573,34 +20166,6 @@ __metadata: languageName: node linkType: hard -"define-property@npm:^0.2.5": - version: 0.2.5 - resolution: "define-property@npm:0.2.5" - dependencies: - is-descriptor: ^0.1.0 - checksum: 85af107072b04973b13f9e4128ab74ddfda48ec7ad2e54b193c0ffb57067c4ce5b7786a7b4ae1f24bd03e87c5d18766b094571810b314d7540f86d4354dbd394 - languageName: node - linkType: hard - -"define-property@npm:^1.0.0": - version: 1.0.0 - resolution: "define-property@npm:1.0.0" - dependencies: - is-descriptor: ^1.0.0 - checksum: 5fbed11dace44dd22914035ba9ae83ad06008532ca814d7936a53a09e897838acdad5b108dd0688cc8d2a7cf0681acbe00ee4136cf36743f680d10517379350a - languageName: node - linkType: hard - -"define-property@npm:^2.0.2": - version: 2.0.2 - resolution: "define-property@npm:2.0.2" - dependencies: - is-descriptor: ^1.0.2 - isobject: ^3.0.1 - checksum: 3217ed53fc9eed06ba8da6f4d33e28c68a82e2f2a8ab4d562c4920d8169a166fe7271453675e6c69301466f36a65d7f47edf0cf7f474b9aa52a5ead9c1b13c99 - languageName: node - linkType: hard - "del@npm:^6.0.0": version: 6.1.1 resolution: "del@npm:6.1.1" @@ -19468,13 +21033,6 @@ __metadata: languageName: node linkType: hard -"esbuild-android-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-android-64@npm:0.14.54" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "esbuild-android-64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-android-64@npm:0.15.18" @@ -19482,13 +21040,6 @@ __metadata: languageName: node linkType: hard -"esbuild-android-arm64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-android-arm64@npm:0.14.54" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "esbuild-android-arm64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-android-arm64@npm:0.15.18" @@ -19496,13 +21047,6 @@ __metadata: languageName: node linkType: hard -"esbuild-darwin-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-darwin-64@npm:0.14.54" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "esbuild-darwin-64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-darwin-64@npm:0.15.18" @@ -19510,13 +21054,6 @@ __metadata: languageName: node linkType: hard -"esbuild-darwin-arm64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-darwin-arm64@npm:0.14.54" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "esbuild-darwin-arm64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-darwin-arm64@npm:0.15.18" @@ -19524,13 +21061,6 @@ __metadata: languageName: node linkType: hard -"esbuild-freebsd-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-freebsd-64@npm:0.14.54" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "esbuild-freebsd-64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-freebsd-64@npm:0.15.18" @@ -19538,13 +21068,6 @@ __metadata: languageName: node linkType: hard -"esbuild-freebsd-arm64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-freebsd-arm64@npm:0.14.54" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "esbuild-freebsd-arm64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-freebsd-arm64@npm:0.15.18" @@ -19552,13 +21075,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-32@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-32@npm:0.14.54" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "esbuild-linux-32@npm:0.15.18": version: 0.15.18 resolution: "esbuild-linux-32@npm:0.15.18" @@ -19566,13 +21082,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-64@npm:0.14.54" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "esbuild-linux-64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-linux-64@npm:0.15.18" @@ -19580,13 +21089,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-arm64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-arm64@npm:0.14.54" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "esbuild-linux-arm64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-linux-arm64@npm:0.15.18" @@ -19594,13 +21096,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-arm@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-arm@npm:0.14.54" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "esbuild-linux-arm@npm:0.15.18": version: 0.15.18 resolution: "esbuild-linux-arm@npm:0.15.18" @@ -19608,13 +21103,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-mips64le@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-mips64le@npm:0.14.54" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "esbuild-linux-mips64le@npm:0.15.18": version: 0.15.18 resolution: "esbuild-linux-mips64le@npm:0.15.18" @@ -19622,13 +21110,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-ppc64le@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-ppc64le@npm:0.14.54" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "esbuild-linux-ppc64le@npm:0.15.18": version: 0.15.18 resolution: "esbuild-linux-ppc64le@npm:0.15.18" @@ -19636,13 +21117,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-riscv64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-riscv64@npm:0.14.54" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "esbuild-linux-riscv64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-linux-riscv64@npm:0.15.18" @@ -19650,13 +21124,6 @@ __metadata: languageName: node linkType: hard -"esbuild-linux-s390x@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-linux-s390x@npm:0.14.54" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "esbuild-linux-s390x@npm:0.15.18": version: 0.15.18 resolution: "esbuild-linux-s390x@npm:0.15.18" @@ -19680,13 +21147,6 @@ __metadata: languageName: node linkType: hard -"esbuild-netbsd-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-netbsd-64@npm:0.14.54" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "esbuild-netbsd-64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-netbsd-64@npm:0.15.18" @@ -19694,13 +21154,6 @@ __metadata: languageName: node linkType: hard -"esbuild-openbsd-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-openbsd-64@npm:0.14.54" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "esbuild-openbsd-64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-openbsd-64@npm:0.15.18" @@ -19708,13 +21161,6 @@ __metadata: languageName: node linkType: hard -"esbuild-sunos-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-sunos-64@npm:0.14.54" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "esbuild-sunos-64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-sunos-64@npm:0.15.18" @@ -19722,13 +21168,6 @@ __metadata: languageName: node linkType: hard -"esbuild-windows-32@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-windows-32@npm:0.14.54" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "esbuild-windows-32@npm:0.15.18": version: 0.15.18 resolution: "esbuild-windows-32@npm:0.15.18" @@ -19736,13 +21175,6 @@ __metadata: languageName: node linkType: hard -"esbuild-windows-64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-windows-64@npm:0.14.54" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "esbuild-windows-64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-windows-64@npm:0.15.18" @@ -19750,13 +21182,6 @@ __metadata: languageName: node linkType: hard -"esbuild-windows-arm64@npm:0.14.54": - version: 0.14.54 - resolution: "esbuild-windows-arm64@npm:0.14.54" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "esbuild-windows-arm64@npm:0.15.18": version: 0.15.18 resolution: "esbuild-windows-arm64@npm:0.15.18" @@ -19764,81 +21189,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.14.1, esbuild@npm:^0.14.10": - version: 0.14.54 - resolution: "esbuild@npm:0.14.54" - dependencies: - "@esbuild/linux-loong64": 0.14.54 - esbuild-android-64: 0.14.54 - esbuild-android-arm64: 0.14.54 - esbuild-darwin-64: 0.14.54 - esbuild-darwin-arm64: 0.14.54 - esbuild-freebsd-64: 0.14.54 - esbuild-freebsd-arm64: 0.14.54 - esbuild-linux-32: 0.14.54 - esbuild-linux-64: 0.14.54 - esbuild-linux-arm: 0.14.54 - esbuild-linux-arm64: 0.14.54 - esbuild-linux-mips64le: 0.14.54 - esbuild-linux-ppc64le: 0.14.54 - esbuild-linux-riscv64: 0.14.54 - esbuild-linux-s390x: 0.14.54 - esbuild-netbsd-64: 0.14.54 - esbuild-openbsd-64: 0.14.54 - esbuild-sunos-64: 0.14.54 - esbuild-windows-32: 0.14.54 - esbuild-windows-64: 0.14.54 - esbuild-windows-arm64: 0.14.54 - dependenciesMeta: - "@esbuild/linux-loong64": - optional: true - esbuild-android-64: - optional: true - esbuild-android-arm64: - optional: true - esbuild-darwin-64: - optional: true - esbuild-darwin-arm64: - optional: true - esbuild-freebsd-64: - optional: true - esbuild-freebsd-arm64: - optional: true - esbuild-linux-32: - optional: true - esbuild-linux-64: - optional: true - esbuild-linux-arm: - optional: true - esbuild-linux-arm64: - optional: true - esbuild-linux-mips64le: - optional: true - esbuild-linux-ppc64le: - optional: true - esbuild-linux-riscv64: - optional: true - esbuild-linux-s390x: - optional: true - esbuild-netbsd-64: - optional: true - esbuild-openbsd-64: - optional: true - esbuild-sunos-64: - optional: true - esbuild-windows-32: - optional: true - esbuild-windows-64: - optional: true - esbuild-windows-arm64: - optional: true - bin: - esbuild: bin/esbuild - checksum: 49e360b1185c797f5ca3a7f5f0a75121494d97ddf691f65ed1796e6257d318f928342a97f559bb8eced6a90cf604dd22db4a30e0dbbf15edd9dbf22459b639af - languageName: node - linkType: hard - -"esbuild@npm:^0.15.6": +"esbuild@npm:^0.15.0, esbuild@npm:^0.15.6": version: 0.15.18 resolution: "esbuild@npm:0.15.18" dependencies: @@ -20189,6 +21540,17 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-testing-library@npm:^5.9.1": + version: 5.9.1 + resolution: "eslint-plugin-testing-library@npm:5.9.1" + dependencies: + "@typescript-eslint/utils": ^5.13.0 + peerDependencies: + eslint: ^7.5.0 || ^8.0.0 + checksum: d09f9486945807e9587d52b6979117bc41b750df741567381a06219671096afb318696a0e0db63e253e150fead40e77ef9653ee00f1dda83fc8920e3b3c47107 + languageName: node + linkType: hard + "eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" @@ -20373,7 +21735,7 @@ __metadata: languageName: node linkType: hard -"estree-walker@npm:^2.0.1": +"estree-walker@npm:^2.0.1, estree-walker@npm:^2.0.2": version: 2.0.2 resolution: "estree-walker@npm:2.0.2" checksum: 6151e6f9828abe2259e57f5fd3761335bb0d2ebd76dc1a01048ccee22fabcfef3c0859300f6d83ff0d1927849368775ec5a6d265dde2f6de5a1be1721cd94efc @@ -20458,7 +21820,7 @@ __metadata: languageName: node linkType: hard -"events@npm:^3.0.0, events@npm:^3.2.0, events@npm:^3.3.0": +"events@npm:3.3.0, events@npm:^3.0.0, events@npm:^3.2.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" checksum: f6f487ad2198aa41d878fa31452f1a3c00958f46e9019286ff4787c84aac329332ab45c9cdc8c445928fc6d7ded294b9e005a7fce9426488518017831b272780 @@ -20573,7 +21935,7 @@ __metadata: react-router-dom: ^6.3.0 react-use: ^17.2.4 start-server-and-test: ^1.10.11 - zen-observable: ^0.8.15 + zen-observable: ^0.10.0 languageName: unknown linkType: soft @@ -20607,7 +21969,11 @@ __metadata: "@backstage/plugin-azure-sites-backend": "workspace:^" "@backstage/plugin-badges-backend": "workspace:^" "@backstage/plugin-catalog-backend": "workspace:^" + "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-code-coverage-backend": "workspace:^" + "@backstage/plugin-events-backend": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + "@backstage/plugin-explore-backend": "workspace:^" "@backstage/plugin-graphql-backend": "workspace:^" "@backstage/plugin-jenkins-backend": "workspace:^" "@backstage/plugin-kafka-backend": "workspace:^" @@ -20749,21 +22115,6 @@ __metadata: languageName: node linkType: hard -"expand-brackets@npm:^2.1.4": - version: 2.1.4 - resolution: "expand-brackets@npm:2.1.4" - dependencies: - debug: ^2.3.3 - define-property: ^0.2.5 - extend-shallow: ^2.0.1 - posix-character-classes: ^0.1.0 - regex-not: ^1.0.0 - snapdragon: ^0.8.1 - to-regex: ^3.0.1 - checksum: 1781d422e7edfa20009e2abda673cadb040a6037f0bd30fcd7357304f4f0c284afd420d7622722ca4a016f39b6d091841ab57b401c1f7e2e5131ac65b9f14fa1 - languageName: node - linkType: hard - "expand-template@npm:^2.0.3": version: 2.0.3 resolution: "expand-template@npm:2.0.3" @@ -20877,25 +22228,6 @@ __metadata: languageName: node linkType: hard -"extend-shallow@npm:^2.0.1": - version: 2.0.1 - resolution: "extend-shallow@npm:2.0.1" - dependencies: - is-extendable: ^0.1.0 - checksum: 8fb58d9d7a511f4baf78d383e637bd7d2e80843bd9cd0853649108ea835208fb614da502a553acc30208e1325240bb7cc4a68473021612496bb89725483656d8 - languageName: node - linkType: hard - -"extend-shallow@npm:^3.0.0, extend-shallow@npm:^3.0.2": - version: 3.0.2 - resolution: "extend-shallow@npm:3.0.2" - dependencies: - assign-symbols: ^1.0.0 - is-extendable: ^1.0.1 - checksum: a920b0cd5838a9995ace31dfd11ab5e79bf6e295aa566910ce53dff19f4b1c0fda2ef21f26b28586c7a2450ca2b42d97bd8c0f5cec9351a819222bf861e02461 - languageName: node - linkType: hard - "extend@npm:3.0.2, extend@npm:^3.0.0, extend@npm:^3.0.2, extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -20921,22 +22253,6 @@ __metadata: languageName: node linkType: hard -"extglob@npm:^2.0.4": - version: 2.0.4 - resolution: "extglob@npm:2.0.4" - dependencies: - array-unique: ^0.3.2 - define-property: ^1.0.0 - expand-brackets: ^2.1.4 - extend-shallow: ^2.0.1 - fragment-cache: ^0.2.1 - regex-not: ^1.0.0 - snapdragon: ^0.8.1 - to-regex: ^3.0.1 - checksum: a41531b8934735b684cef5e8c5a01d0f298d7d384500ceca38793a9ce098125aab04ee73e2d75d5b2901bc5dddd2b64e1b5e3bf19139ea48bac52af4a92f1d00 - languageName: node - linkType: hard - "extract-files@npm:^11.0.0": version: 11.0.0 resolution: "extract-files@npm:11.0.0" @@ -21081,6 +22397,17 @@ __metadata: languageName: node linkType: hard +"fast-xml-parser@npm:4.0.11": + version: 4.0.11 + resolution: "fast-xml-parser@npm:4.0.11" + dependencies: + strnum: ^1.0.5 + bin: + fxparser: src/cli/cli.js + checksum: d8a08e4d5597e0fc00a86735195872eeb03008913e298830941516f3766e16ee555e2d431acc92e1dda887938edc445252ec5b59494aab60a8389888bd13719c + languageName: node + linkType: hard + "fastest-stable-stringify@npm:^2.0.2": version: 2.0.2 resolution: "fastest-stable-stringify@npm:2.0.2" @@ -21221,18 +22548,6 @@ __metadata: languageName: node linkType: hard -"fill-range@npm:^4.0.0": - version: 4.0.0 - resolution: "fill-range@npm:4.0.0" - dependencies: - extend-shallow: ^2.0.1 - is-number: ^3.0.0 - repeat-string: ^1.6.1 - to-regex-range: ^2.1.0 - checksum: dbb5102467786ab42bc7a3ec7380ae5d6bfd1b5177b2216de89e4a541193f8ba599a6db84651bd2c58c8921db41b8cc3d699ea83b477342d3ce404020f73c298 - languageName: node - linkType: hard - "fill-range@npm:^7.0.1": version: 7.0.1 resolution: "fill-range@npm:7.0.1" @@ -21431,13 +22746,6 @@ __metadata: languageName: node linkType: hard -"for-in@npm:^1.0.2": - version: 1.0.2 - resolution: "for-in@npm:1.0.2" - checksum: 09f4ae93ce785d253ac963d94c7f3432d89398bf25ac7a24ed034ca393bf74380bdeccc40e0f2d721a895e54211b07c8fad7132e8157827f6f7f059b70b4043d - languageName: node - linkType: hard - "foreach@npm:^2.0.4, foreach@npm:^2.0.5": version: 2.0.5 resolution: "foreach@npm:2.0.5" @@ -21597,15 +22905,6 @@ __metadata: languageName: node linkType: hard -"fragment-cache@npm:^0.2.1": - version: 0.2.1 - resolution: "fragment-cache@npm:0.2.1" - dependencies: - map-cache: ^0.2.2 - checksum: 1cbbd0b0116b67d5790175de0038a11df23c1cd2e8dcdbade58ebba5594c2d641dade6b4f126d82a7b4a6ffc2ea12e3d387dbb64ea2ae97cf02847d436f60fdc - languageName: node - linkType: hard - "fresh@npm:0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -21971,13 +23270,6 @@ __metadata: languageName: node linkType: hard -"get-value@npm:^2.0.3, get-value@npm:^2.0.6": - version: 2.0.6 - resolution: "get-value@npm:2.0.6" - checksum: 5c3b99cb5398ea8016bf46ff17afc5d1d286874d2ad38ca5edb6e87d75c0965b0094cb9a9dddef2c59c23d250702323539a7fbdd870620db38c7e7d7ec87c1eb - languageName: node - linkType: hard - "getopts@npm:2.3.0": version: 2.3.0 resolution: "getopts@npm:2.3.0" @@ -22082,7 +23374,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.0, glob@npm:^8.0.1": +"glob@npm:^8.0.0, glob@npm:^8.0.1, glob@npm:^8.0.3": version: 8.0.3 resolution: "glob@npm:8.0.3" dependencies: @@ -22596,45 +23888,6 @@ __metadata: languageName: node linkType: hard -"has-value@npm:^0.3.1": - version: 0.3.1 - resolution: "has-value@npm:0.3.1" - dependencies: - get-value: ^2.0.3 - has-values: ^0.1.4 - isobject: ^2.0.0 - checksum: 29e2a1e6571dad83451b769c7ce032fce6009f65bccace07c2962d3ad4d5530b6743d8f3229e4ecf3ea8e905d23a752c5f7089100c1f3162039fa6dc3976558f - languageName: node - linkType: hard - -"has-value@npm:^1.0.0": - version: 1.0.0 - resolution: "has-value@npm:1.0.0" - dependencies: - get-value: ^2.0.6 - has-values: ^1.0.0 - isobject: ^3.0.0 - checksum: b9421d354e44f03d3272ac39fd49f804f19bc1e4fa3ceef7745df43d6b402053f828445c03226b21d7d934a21ac9cf4bc569396dc312f496ddff873197bbd847 - languageName: node - linkType: hard - -"has-values@npm:^0.1.4": - version: 0.1.4 - resolution: "has-values@npm:0.1.4" - checksum: ab1c4bcaf811ccd1856c11cfe90e62fca9e2b026ebe474233a3d282d8d67e3b59ed85b622c7673bac3db198cb98bd1da2b39300a2f98e453729b115350af49bc - languageName: node - linkType: hard - -"has-values@npm:^1.0.0": - version: 1.0.0 - resolution: "has-values@npm:1.0.0" - dependencies: - is-number: ^3.0.0 - kind-of: ^4.0.0 - checksum: 77e6693f732b5e4cf6c38dfe85fdcefad0fab011af74995c3e83863fabf5e3a836f406d83565816baa0bc0a523c9410db8b990fe977074d61aeb6d8f4fcffa11 - languageName: node - linkType: hard - "has@npm:^1.0.3": version: 1.0.3 resolution: "has@npm:1.0.3" @@ -22972,7 +24225,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-middleware@npm:^2.0.0, http-proxy-middleware@npm:^2.0.3": +"http-proxy-middleware@npm:^2.0.0, http-proxy-middleware@npm:^2.0.3, http-proxy-middleware@npm:^2.0.6": version: 2.0.6 resolution: "http-proxy-middleware@npm:2.0.6" dependencies: @@ -23344,7 +24597,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3": +"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3, inherits@npm:~2.0.4": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 @@ -23560,24 +24813,6 @@ __metadata: languageName: node linkType: hard -"is-accessor-descriptor@npm:^0.1.6": - version: 0.1.6 - resolution: "is-accessor-descriptor@npm:0.1.6" - dependencies: - kind-of: ^3.0.2 - checksum: 3d629a086a9585bc16a83a8e8a3416f400023301855cafb7ccc9a1d63145b7480f0ad28877dcc2cce09492c4ec1c39ef4c071996f24ee6ac626be4217b8ffc8a - languageName: node - linkType: hard - -"is-accessor-descriptor@npm:^1.0.0": - version: 1.0.0 - resolution: "is-accessor-descriptor@npm:1.0.0" - dependencies: - kind-of: ^6.0.0 - checksum: 8e475968e9b22f9849343c25854fa24492dbe8ba0dea1a818978f9f1b887339190b022c9300d08c47fe36f1b913d70ce8cbaca00369c55a56705fdb7caed37fe - languageName: node - linkType: hard - "is-alphabetical@npm:^1.0.0": version: 1.0.4 resolution: "is-alphabetical@npm:1.0.4" @@ -23661,13 +24896,6 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^1.1.5": - version: 1.1.6 - resolution: "is-buffer@npm:1.1.6" - checksum: 4a186d995d8bbf9153b4bd9ff9fd04ae75068fe695d29025d25e592d9488911eeece84eefbd8fa41b8ddcc0711058a71d4c466dcf6f1f6e1d83830052d8ca707 - languageName: node - linkType: hard - "is-buffer@npm:^2.0.0": version: 2.0.4 resolution: "is-buffer@npm:2.0.4" @@ -23722,24 +24950,6 @@ __metadata: languageName: node linkType: hard -"is-data-descriptor@npm:^0.1.4": - version: 0.1.4 - resolution: "is-data-descriptor@npm:0.1.4" - dependencies: - kind-of: ^3.0.2 - checksum: 5c622e078ba933a78338ae398a3d1fc5c23332b395312daf4f74bab4afb10d061cea74821add726cb4db8b946ba36217ee71a24fe71dd5bca4632edb7f6aad87 - languageName: node - linkType: hard - -"is-data-descriptor@npm:^1.0.0": - version: 1.0.0 - resolution: "is-data-descriptor@npm:1.0.0" - dependencies: - kind-of: ^6.0.0 - checksum: e705e6816241c013b05a65dc452244ee378d1c3e3842bd140beabe6e12c0d700ef23c91803f971aa7b091fb0573c5da8963af34a2b573337d87bc3e1f53a4e6d - languageName: node - linkType: hard - "is-date-object@npm:^1.0.1": version: 1.0.5 resolution: "is-date-object@npm:1.0.5" @@ -23763,28 +24973,6 @@ __metadata: languageName: node linkType: hard -"is-descriptor@npm:^0.1.0": - version: 0.1.6 - resolution: "is-descriptor@npm:0.1.6" - dependencies: - is-accessor-descriptor: ^0.1.6 - is-data-descriptor: ^0.1.4 - kind-of: ^5.0.0 - checksum: 0f780c1b46b465f71d970fd7754096ffdb7b69fd8797ca1f5069c163eaedcd6a20ec4a50af669075c9ebcfb5266d2e53c8b227e485eefdb0d1fee09aa1dd8ab6 - languageName: node - linkType: hard - -"is-descriptor@npm:^1.0.0, is-descriptor@npm:^1.0.2": - version: 1.0.2 - resolution: "is-descriptor@npm:1.0.2" - dependencies: - is-accessor-descriptor: ^1.0.0 - is-data-descriptor: ^1.0.0 - kind-of: ^6.0.2 - checksum: 2ed623560bee035fb67b23e32ce885700bef8abe3fbf8c909907d86507b91a2c89a9d3a4d835a4d7334dd5db0237a0aeae9ca109c1e4ef1c0e7b577c0846ab5a - languageName: node - linkType: hard - "is-docker@npm:^2.0.0, is-docker@npm:^2.1.1": version: 2.2.1 resolution: "is-docker@npm:2.2.1" @@ -23794,22 +24982,6 @@ __metadata: languageName: node linkType: hard -"is-extendable@npm:^0.1.0, is-extendable@npm:^0.1.1": - version: 0.1.1 - resolution: "is-extendable@npm:0.1.1" - checksum: 3875571d20a7563772ecc7a5f36cb03167e9be31ad259041b4a8f73f33f885441f778cee1f1fe0085eb4bc71679b9d8c923690003a36a6a5fdf8023e6e3f0672 - languageName: node - linkType: hard - -"is-extendable@npm:^1.0.1": - version: 1.0.1 - resolution: "is-extendable@npm:1.0.1" - dependencies: - is-plain-object: ^2.0.4 - checksum: db07bc1e9de6170de70eff7001943691f05b9d1547730b11be01c0ebfe67362912ba743cf4be6fd20a5e03b4180c685dad80b7c509fe717037e3eee30ad8e84f - languageName: node - linkType: hard - "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -23954,15 +25126,6 @@ __metadata: languageName: node linkType: hard -"is-number@npm:^3.0.0": - version: 3.0.0 - resolution: "is-number@npm:3.0.0" - dependencies: - kind-of: ^3.0.2 - checksum: 0c62bf8e9d72c4dd203a74d8cfc751c746e75513380fef420cda8237e619a988ee43e678ddb23c87ac24d91ac0fe9f22e4ffb1301a50310c697e9d73ca3994e9 - languageName: node - linkType: hard - "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -24005,7 +25168,7 @@ __metadata: languageName: node linkType: hard -"is-plain-object@npm:^2.0.3, is-plain-object@npm:^2.0.4": +"is-plain-object@npm:^2.0.4": version: 2.0.4 resolution: "is-plain-object@npm:2.0.4" dependencies: @@ -24056,7 +25219,7 @@ __metadata: languageName: node linkType: hard -"is-reference@npm:^1.2.1": +"is-reference@npm:1.2.1": version: 1.2.1 resolution: "is-reference@npm:1.2.1" dependencies: @@ -24234,7 +25397,7 @@ __metadata: languageName: node linkType: hard -"is-windows@npm:^1.0.0, is-windows@npm:^1.0.1, is-windows@npm:^1.0.2": +"is-windows@npm:^1.0.0, is-windows@npm:^1.0.1": version: 1.0.2 resolution: "is-windows@npm:1.0.2" checksum: 438b7e52656fe3b9b293b180defb4e448088e7023a523ec21a91a80b9ff8cdb3377ddb5b6e60f7c7de4fa8b63ab56e121b6705fe081b3cf1b828b0a380009ad7 @@ -24257,13 +25420,6 @@ __metadata: languageName: node linkType: hard -"isarray@npm:1.0.0, isarray@npm:^1.0.0, isarray@npm:~1.0.0": - version: 1.0.0 - resolution: "isarray@npm:1.0.0" - checksum: f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab - languageName: node - linkType: hard - "isarray@npm:2.0.1": version: 2.0.1 resolution: "isarray@npm:2.0.1" @@ -24271,6 +25427,13 @@ __metadata: languageName: node linkType: hard +"isarray@npm:^1.0.0, isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: f032df8e02dce8ec565cf2eb605ea939bdccea528dbcf565cdf92bfa2da9110461159d86a537388ef1acef8815a330642d7885b29010e8f7eac967c9993b65ab + languageName: node + linkType: hard + "isbinaryfile@npm:^4.0.10, isbinaryfile@npm:^4.0.8": version: 4.0.10 resolution: "isbinaryfile@npm:4.0.10" @@ -24292,16 +25455,7 @@ __metadata: languageName: node linkType: hard -"isobject@npm:^2.0.0": - version: 2.1.0 - resolution: "isobject@npm:2.1.0" - dependencies: - isarray: 1.0.0 - checksum: 811c6f5a866877d31f0606a88af4a45f282544de886bf29f6a34c46616a1ae2ed17076cc6bf34c0128f33eecf7e1fcaa2c82cf3770560d3e26810894e96ae79f - languageName: node - linkType: hard - -"isobject@npm:^3.0.0, isobject@npm:^3.0.1": +"isobject@npm:^3.0.1": version: 3.0.1 resolution: "isobject@npm:3.0.1" checksum: db85c4c970ce30693676487cca0e61da2ca34e8d4967c2e1309143ff910c207133a969f9e4ddb2dc6aba670aabce4e0e307146c310350b298e74a31f7d464703 @@ -25105,7 +26259,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.0, js-yaml@npm:^3.14.1, js-yaml@npm:^3.6.1, js-yaml@npm:^3.8.3": +"js-yaml@npm:^3.10.0, js-yaml@npm:^3.13.0, js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.1, js-yaml@npm:^3.6.1, js-yaml@npm:^3.8.3": version: 3.14.1 resolution: "js-yaml@npm:3.14.1" dependencies: @@ -25166,9 +26320,9 @@ __metadata: languageName: node linkType: hard -"jscodeshift@npm:^0.13.0": - version: 0.13.1 - resolution: "jscodeshift@npm:0.13.1" +"jscodeshift@npm:^0.14.0": + version: 0.14.0 + resolution: "jscodeshift@npm:0.14.0" dependencies: "@babel/core": ^7.13.16 "@babel/parser": ^7.13.16 @@ -25183,17 +26337,17 @@ __metadata: chalk: ^4.1.2 flow-parser: 0.* graceful-fs: ^4.2.4 - micromatch: ^3.1.10 + micromatch: ^4.0.4 neo-async: ^2.5.0 node-dir: ^0.1.17 - recast: ^0.20.4 + recast: ^0.21.0 temp: ^0.8.4 write-file-atomic: ^2.3.0 peerDependencies: "@babel/preset-env": ^7.1.6 bin: jscodeshift: bin/jscodeshift.js - checksum: 1c35938de5fc29cafec80e2c37d5c3411f85cd5d40e0243b52f2da0c1ab4b659daddfd62de558eca5d562303616f7838097727b651f4ad8e32b1e96f169cdd76 + checksum: 54ea6d639455883336f80b38a70648821c88b7942315dc0fbab01bc34a9ad0f0f78e3bd69304b5ab167e4262d6ed7e6284c6d32525ab01c89d9118df89b3e2a0 languageName: node linkType: hard @@ -25381,16 +26535,16 @@ __metadata: languageName: node linkType: hard -"json-schema-library@npm:^7.0.0": - version: 7.4.1 - resolution: "json-schema-library@npm:7.4.1" +"json-schema-library@npm:^7.3.9": + version: 7.3.9 + resolution: "json-schema-library@npm:7.3.9" dependencies: "@sagold/json-pointer": ^5.0.0 "@sagold/json-query": ^6.0.0 deepmerge: ^4.2.2 fast-deep-equal: ^3.1.3 valid-url: ^1.0.9 - checksum: 5d64cc325d43b0d661b8a841e06345eda6bcfb11f0af703bcc39475ea9389d22fbfcbd1d70e5b5cf5cdec76dd2edcb52e45dddb131aed6ffc97544ef5660d847 + checksum: de40dd4d5f1ce438fa45703402716f2cf543b8e89d4ffc3618045f771f6f749928984905db761e9cf7e8602d213f4578c3e983a40e0d49013c0fea1488d397a8 languageName: node linkType: hard @@ -25816,17 +26970,7 @@ __metadata: languageName: node linkType: hard -"keyv-memcache@npm:^1.2.5": - version: 1.3.3 - resolution: "keyv-memcache@npm:1.3.3" - dependencies: - json-buffer: ^3.0.1 - memjs: ^1.3.0 - checksum: ddef602c027a325fc1d18917788799e79cf73f46cbbfefeeda893ca585531544aa4b50de6601c1c5e0427a94bb52ad64e9144c919105b26aac0b173ebcdabc28 - languageName: node - linkType: hard - -"keyv@npm:^4.0.0, keyv@npm:^4.0.3": +"keyv@npm:^4.0.0, keyv@npm:^4.5.2": version: 4.5.2 resolution: "keyv@npm:4.5.2" dependencies: @@ -25835,32 +26979,7 @@ __metadata: languageName: node linkType: hard -"kind-of@npm:^3.0.2, kind-of@npm:^3.0.3, kind-of@npm:^3.2.0": - version: 3.2.2 - resolution: "kind-of@npm:3.2.2" - dependencies: - is-buffer: ^1.1.5 - checksum: e898df8ca2f31038f27d24f0b8080da7be274f986bc6ed176f37c77c454d76627619e1681f6f9d2e8d2fd7557a18ecc419a6bb54e422abcbb8da8f1a75e4b386 - languageName: node - linkType: hard - -"kind-of@npm:^4.0.0": - version: 4.0.0 - resolution: "kind-of@npm:4.0.0" - dependencies: - is-buffer: ^1.1.5 - checksum: 1b9e7624a8771b5a2489026e820f3bbbcc67893e1345804a56b23a91e9069965854d2a223a7c6ee563c45be9d8c6ff1ef87f28ed5f0d1a8d00d9dcbb067c529f - languageName: node - linkType: hard - -"kind-of@npm:^5.0.0": - version: 5.1.0 - resolution: "kind-of@npm:5.1.0" - checksum: f2a0102ae0cf19c4a953397e552571bad2b588b53282874f25fca7236396e650e2db50d41f9f516bd402536e4df968dbb51b8e69e4d5d4a7173def78448f7bab - languageName: node - linkType: hard - -"kind-of@npm:^6.0.0, kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": +"kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": version: 6.0.3 resolution: "kind-of@npm:6.0.3" checksum: 3ab01e7b1d440b22fe4c31f23d8d38b4d9b91d9f291df683476576493d5dfd2e03848a8b05813dd0c3f0e835bc63f433007ddeceb71f05cb25c45ae1b19c6d3b @@ -26738,7 +27857,7 @@ __metadata: languageName: node linkType: hard -"magic-string@npm:^0.26.6": +"magic-string@npm:^0.26.4, magic-string@npm:^0.26.6": version: 0.26.7 resolution: "magic-string@npm:0.26.7" dependencies: @@ -26830,7 +27949,7 @@ __metadata: languageName: node linkType: hard -"map-cache@npm:^0.2.0, map-cache@npm:^0.2.2": +"map-cache@npm:^0.2.0": version: 0.2.2 resolution: "map-cache@npm:0.2.2" checksum: 3067cea54285c43848bb4539f978a15dedc63c03022abeec6ef05c8cb6829f920f13b94bcaf04142fc6a088318e564c4785704072910d120d55dbc2e0c421969 @@ -26858,15 +27977,6 @@ __metadata: languageName: node linkType: hard -"map-visit@npm:^1.0.0": - version: 1.0.0 - resolution: "map-visit@npm:1.0.0" - dependencies: - object-visit: ^1.0.0 - checksum: c27045a5021c344fc19b9132eb30313e441863b2951029f8f8b66f79d3d8c1e7e5091578075a996f74e417479506fe9ede28c44ca7bc351a61c9d8073daec36a - languageName: node - linkType: hard - "markdown-it-anchor@npm:^8.4.1": version: 8.6.4 resolution: "markdown-it-anchor@npm:8.6.4" @@ -27578,27 +28688,6 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^3.1.10": - version: 3.1.10 - resolution: "micromatch@npm:3.1.10" - dependencies: - arr-diff: ^4.0.0 - array-unique: ^0.3.2 - braces: ^2.3.1 - define-property: ^2.0.2 - extend-shallow: ^3.0.2 - extglob: ^2.0.4 - fragment-cache: ^0.2.1 - kind-of: ^6.0.2 - nanomatch: ^1.2.9 - object.pick: ^1.3.0 - regex-not: ^1.0.0 - snapdragon: ^0.8.1 - to-regex: ^3.0.2 - checksum: ad226cba4daa95b4eaf47b2ca331c8d2e038d7b41ae7ed0697cde27f3f1d6142881ab03d4da51b65d9d315eceb5e4cdddb3fbb55f5f72cfa19cf3ea469d054dc - languageName: node - linkType: hard - "micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": version: 4.0.5 resolution: "micromatch@npm:4.0.5" @@ -27774,16 +28863,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:5.1.0": - version: 5.1.0 - resolution: "minimatch@npm:5.1.0" - dependencies: - brace-expansion: ^2.0.1 - checksum: 15ce53d31a06361e8b7a629501b5c75491bc2b59712d53e802b1987121d91b433d73fcc5be92974fde66b2b51d8fb28d75a9ae900d249feb792bb1ba2a4f0a90 - languageName: node - linkType: hard - -"minimatch@npm:^5.0.0, minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": +"minimatch@npm:^5.0.0, minimatch@npm:^5.0.1, minimatch@npm:^5.1.0, minimatch@npm:^5.1.1": version: 5.1.1 resolution: "minimatch@npm:5.1.1" dependencies: @@ -27914,16 +28994,6 @@ __metadata: languageName: node linkType: hard -"mixin-deep@npm:^1.2.0": - version: 1.3.2 - resolution: "mixin-deep@npm:1.3.2" - dependencies: - for-in: ^1.0.2 - is-extendable: ^1.0.1 - checksum: 820d5a51fcb7479f2926b97f2c3bb223546bc915e6b3a3eb5d906dda871bba569863595424a76682f2b15718252954644f3891437cb7e3f220949bed54b1750d - languageName: node - linkType: hard - "mixme@npm:^0.5.1": version: 0.5.4 resolution: "mixme@npm:0.5.4" @@ -28102,6 +29172,40 @@ __metadata: languageName: node linkType: hard +"msw@npm:^0.49.0": + version: 0.49.1 + resolution: "msw@npm:0.49.1" + dependencies: + "@mswjs/cookies": ^0.2.2 + "@mswjs/interceptors": ^0.17.5 + "@open-draft/until": ^1.0.3 + "@types/cookie": ^0.4.1 + "@types/js-levenshtein": ^1.1.1 + chalk: 4.1.1 + chokidar: ^3.4.2 + cookie: ^0.4.2 + graphql: ^15.0.0 || ^16.0.0 + headers-polyfill: ^3.1.0 + inquirer: ^8.2.0 + is-node-process: ^1.0.1 + js-levenshtein: ^1.1.6 + node-fetch: ^2.6.7 + outvariant: ^1.3.0 + path-to-regexp: ^6.2.0 + strict-event-emitter: ^0.2.6 + type-fest: ^2.19.0 + yargs: ^17.3.1 + peerDependencies: + typescript: ">= 4.4.x <= 4.9.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 4d8ebcc21cbe20d73070857653a1e90b1be7c78ac2ea68ae497baa2a5d9bc1fb764ccfb292fc8eda0b6cfabd4542a80bbacb0302e9ba939a0bc94b024848850d + languageName: node + linkType: hard + "multicast-dns@npm:^7.2.4": version: 7.2.4 resolution: "multicast-dns@npm:7.2.4" @@ -28214,25 +29318,6 @@ __metadata: languageName: node linkType: hard -"nanomatch@npm:^1.2.9": - version: 1.2.13 - resolution: "nanomatch@npm:1.2.13" - dependencies: - arr-diff: ^4.0.0 - array-unique: ^0.3.2 - define-property: ^2.0.2 - extend-shallow: ^3.0.2 - fragment-cache: ^0.2.1 - is-windows: ^1.0.2 - kind-of: ^6.0.2 - object.pick: ^1.3.0 - regex-not: ^1.0.0 - snapdragon: ^0.8.1 - to-regex: ^3.0.1 - checksum: 54d4166d6ef08db41252eb4e96d4109ebcb8029f0374f9db873bd91a1f896c32ec780d2a2ea65c0b2d7caf1f28d5e1ea33746a470f32146ac8bba821d80d38d8 - languageName: node - linkType: hard - "napi-build-utils@npm:^1.0.1": version: 1.0.2 resolution: "napi-build-utils@npm:1.0.2" @@ -28290,7 +29375,7 @@ __metadata: languageName: node linkType: hard -"nise@npm:^5.1.1": +"nise@npm:^5.1.1, nise@npm:^5.1.2": version: 5.1.2 resolution: "nise@npm:5.1.2" dependencies: @@ -28395,6 +29480,26 @@ __metadata: languageName: node linkType: hard +"node-gyp@npm:^9.1.0": + version: 9.3.0 + resolution: "node-gyp@npm:9.3.0" + dependencies: + env-paths: ^2.2.0 + glob: ^7.1.4 + graceful-fs: ^4.2.6 + make-fetch-happen: ^10.0.3 + nopt: ^6.0.0 + npmlog: ^6.0.0 + rimraf: ^3.0.2 + semver: ^7.3.5 + tar: ^6.1.2 + which: ^2.0.2 + bin: + node-gyp: bin/node-gyp.js + checksum: 589ddd3ed967724ef425f9624bfa47cf73022640ab3eba6d556e92cdc4ddef33b63fce3a467c93b995a3f61df92eafd3c3d1e8dbe4a2c00c383334487dea99c3 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 9.1.0 resolution: "node-gyp@npm:9.1.0" @@ -28502,6 +29607,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:^6.0.0": + version: 6.0.0 + resolution: "nopt@npm:6.0.0" + dependencies: + abbrev: ^1.0.0 + bin: + nopt: bin/nopt.js + checksum: 82149371f8be0c4b9ec2f863cc6509a7fd0fa729929c009f3a58e4eb0c9e4cae9920e8f1f8eb46e7d032fec8fb01bede7f0f41a67eb3553b7b8e14fa53de1dac + languageName: node + linkType: hard + "normalize-package-data@npm:^2.3.2, normalize-package-data@npm:^2.5.0": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -28766,17 +29882,6 @@ __metadata: languageName: node linkType: hard -"object-copy@npm:^0.1.0": - version: 0.1.0 - resolution: "object-copy@npm:0.1.0" - dependencies: - copy-descriptor: ^0.1.0 - define-property: ^0.2.5 - kind-of: ^3.0.3 - checksum: a9e35f07e3a2c882a7e979090360d1a20ab51d1fa19dfdac3aa8873b328a7c4c7683946ee97c824ae40079d848d6740a3788fa14f2185155dab7ed970a72c783 - languageName: node - linkType: hard - "object-hash@npm:3.0.0, object-hash@npm:^3.0.0": version: 3.0.0 resolution: "object-hash@npm:3.0.0" @@ -28805,15 +29910,6 @@ __metadata: languageName: node linkType: hard -"object-visit@npm:^1.0.0": - version: 1.0.1 - resolution: "object-visit@npm:1.0.1" - dependencies: - isobject: ^3.0.0 - checksum: b0ee07f5bf3bb881b881ff53b467ebbde2b37ebb38649d6944a6cd7681b32eedd99da9bd1e01c55facf81f54ed06b13af61aba6ad87f0052982995e09333f790 - languageName: node - linkType: hard - "object.assign@npm:^4.1.0, object.assign@npm:^4.1.2, object.assign@npm:^4.1.4": version: 4.1.4 resolution: "object.assign@npm:4.1.4" @@ -28858,15 +29954,6 @@ __metadata: languageName: node linkType: hard -"object.pick@npm:^1.3.0": - version: 1.3.0 - resolution: "object.pick@npm:1.3.0" - dependencies: - isobject: ^3.0.1 - checksum: 77fb6eed57c67adf75e9901187e37af39f052ef601cb4480386436561357eb9e459e820762f01fd02c5c1b42ece839ad393717a6d1850d848ee11fbabb3e580a - languageName: node - linkType: hard - "object.values@npm:^1.1.5, object.values@npm:^1.1.6": version: 1.1.6 resolution: "object.values@npm:1.1.6" @@ -29023,7 +30110,7 @@ __metadata: languageName: node linkType: hard -"openid-client@npm:^5.1.3, openid-client@npm:^5.1.6": +"openid-client@npm:^5.1.6, openid-client@npm:^5.2.1": version: 5.3.1 resolution: "openid-client@npm:5.3.1" dependencies: @@ -29562,13 +30649,6 @@ __metadata: languageName: node linkType: hard -"pascalcase@npm:^0.1.1": - version: 0.1.1 - resolution: "pascalcase@npm:0.1.1" - checksum: f83681c3c8ff75fa473a2bb2b113289952f802ff895d435edd717e7cb898b0408cbdb247117a938edcbc5d141020909846cc2b92c47213d764e2a94d2ad2b925 - languageName: node - linkType: hard - "passport-auth0@npm:^1.4.3": version: 1.4.3 resolution: "passport-auth0@npm:1.4.3" @@ -30238,13 +31318,6 @@ __metadata: languageName: node linkType: hard -"posix-character-classes@npm:^0.1.0": - version: 0.1.1 - resolution: "posix-character-classes@npm:0.1.1" - checksum: dedb99913c60625a16050cfed2fb5c017648fc075be41ac18474e1c6c3549ef4ada201c8bd9bd006d36827e289c571b6092e1ef6e756cdbab2fd7046b25c6442 - languageName: node - linkType: hard - "postcss-calc@npm:^8.0.0": version: 8.0.0 resolution: "postcss-calc@npm:8.0.0" @@ -30867,13 +31940,6 @@ __metadata: languageName: node linkType: hard -"promise-any-polyfill@npm:^1.0.1": - version: 1.0.1 - resolution: "promise-any-polyfill@npm:1.0.1" - checksum: 8eccebb8f9278c422cda05083e506964e6c75061b9c6d7fb6f3b7ccb707e5d338138aed3092b8358355a18f8d6dd47c139d0e198aae1eb0c84cd943a35d7ee0c - languageName: node - linkType: hard - "promise-call-limit@npm:^1.0.1": version: 1.0.1 resolution: "promise-call-limit@npm:1.0.1" @@ -31353,6 +32419,20 @@ __metadata: languageName: node linkType: hard +"rc-progress@npm:3.4.1": + version: 3.4.1 + resolution: "rc-progress@npm:3.4.1" + dependencies: + "@babel/runtime": ^7.10.1 + classnames: ^2.2.6 + rc-util: ^5.16.1 + peerDependencies: + react: ">=16.9.0" + react-dom: ">=16.9.0" + checksum: d4dce5231ea29bfa866935a59f05473711f9dfc944f95cc78b0fdcd1508a063983cc58973f54c2b20797f0b0f480c4a2b84aaa2ca185158c14800ec659163880 + languageName: node + linkType: hard + "rc-util@npm:^5.16.1": version: 5.16.1 resolution: "rc-util@npm:5.16.1" @@ -32083,15 +33163,15 @@ __metadata: languageName: node linkType: hard -"recast@npm:^0.20.4": - version: 0.20.5 - resolution: "recast@npm:0.20.5" +"recast@npm:^0.21.0": + version: 0.21.5 + resolution: "recast@npm:0.21.5" dependencies: - ast-types: 0.14.2 + ast-types: 0.15.2 esprima: ~4.0.0 source-map: ~0.6.1 tslib: ^2.0.1 - checksum: 14c35115cd9965950724cb2968f069a247fa79ce890643ab6dc3795c705b363f7b92a45238e9f765387c306763be9955f72047bb9d15b5d60b0a55f9e7912d5a + checksum: 03cc7f57562238ba258d468be67bf7446ce7a707bc87a087891dad15afead46c36e9aaeedf2130e2ab5a465244a9c62bfd4127849761cf8f4085abe2f3e5f485 languageName: node linkType: hard @@ -32274,16 +33354,6 @@ __metadata: languageName: node linkType: hard -"regex-not@npm:^1.0.0, regex-not@npm:^1.0.2": - version: 1.0.2 - resolution: "regex-not@npm:1.0.2" - dependencies: - extend-shallow: ^3.0.2 - safe-regex: ^1.1.0 - checksum: 3081403de79559387a35ef9d033740e41818a559512668cef3d12da4e8a29ef34ee13c8ed1256b07e27ae392790172e8a15c8a06b72962fd4550476cde3d8f77 - languageName: node - linkType: hard - "regexp.prototype.flags@npm:^1.4.3": version: 1.4.3 resolution: "regexp.prototype.flags@npm:1.4.3" @@ -32440,14 +33510,7 @@ __metadata: languageName: node linkType: hard -"repeat-element@npm:^1.1.2": - version: 1.1.4 - resolution: "repeat-element@npm:1.1.4" - checksum: 1edd0301b7edad71808baad226f0890ba709443f03a698224c9ee4f2494c317892dc5211b2ba8cbea7194a9ddbcac01e283bd66de0467ab24ee1fc1a3711d8a9 - languageName: node - linkType: hard - -"repeat-string@npm:^1.5.2, repeat-string@npm:^1.6.1": +"repeat-string@npm:^1.5.2": version: 1.6.1 resolution: "repeat-string@npm:1.6.1" checksum: 1b809fc6db97decdc68f5b12c4d1a671c8e3f65ec4a40c238bc5200e44e85bcc52a54f78268ab9c29fcf5fe4f1343e805420056d1f30fa9a9ee4c2d93e3cc6c0 @@ -32599,13 +33662,6 @@ __metadata: languageName: node linkType: hard -"resolve-url@npm:^0.2.1": - version: 0.2.1 - resolution: "resolve-url@npm:0.2.1" - checksum: 7b7035b9ed6e7bc7d289e90aef1eab5a43834539695dac6416ca6e91f1a94132ae4796bbd173cdacfdc2ade90b5f38a3fb6186bebc1b221cd157777a23b9ad14 - languageName: node - linkType: hard - "resolve.exports@npm:^1.1.0": version: 1.1.0 resolution: "resolve.exports@npm:1.1.0" @@ -32613,7 +33669,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.0": +"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.0": version: 1.22.1 resolution: "resolve@npm:1.22.1" dependencies: @@ -32655,7 +33711,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin": +"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin": version: 1.22.1 resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=07638b" dependencies: @@ -32723,13 +33779,6 @@ __metadata: languageName: node linkType: hard -"ret@npm:~0.1.10": - version: 0.1.15 - resolution: "ret@npm:0.1.15" - checksum: d76a9159eb8c946586567bd934358dfc08a36367b3257f7a3d7255fdd7b56597235af23c6afa0d7f0254159e8051f93c918809962ebd6df24ca2a83dbe4d4151 - languageName: node - linkType: hard - "retry-request@npm:^5.0.0": version: 5.0.0 resolution: "retry-request@npm:5.0.0" @@ -32968,12 +34017,9 @@ __metadata: "@backstage/codemods": "workspace:*" "@backstage/create-app": "workspace:*" "@backstage/errors": "workspace:^" + "@backstage/repo-tools": "workspace:*" "@changesets/cli": ^2.14.0 "@manypkg/get-packages": ^1.1.3 - "@microsoft/api-documenter": ^7.17.11 - "@microsoft/api-extractor": ^7.23.0 - "@microsoft/api-extractor-model": ^7.17.2 - "@microsoft/tsdoc": ^0.14.1 "@octokit/rest": ^19.0.3 "@spotify/prettier-config": ^14.0.0 "@techdocs/cli": "workspace:*" @@ -32985,10 +34031,12 @@ __metadata: e2e-test: "workspace:*" eslint: ^8.6.0 eslint-plugin-notice: ^0.9.10 + eslint-plugin-testing-library: ^5.9.1 fs-extra: 10.1.0 husky: ^8.0.0 lint-staged: ^13.0.0 minimist: ^1.2.5 + node-gyp: ^9.1.0 prettier: ^2.2.1 semver: ^7.3.2 shx: ^0.3.2 @@ -33106,15 +34154,6 @@ __metadata: languageName: node linkType: hard -"safe-regex@npm:^1.1.0": - version: 1.1.0 - resolution: "safe-regex@npm:1.1.0" - dependencies: - ret: ~0.1.10 - checksum: 9a8bba57c87a841f7997b3b951e8e403b1128c1a4fd1182f40cc1a20e2d490593d7c2a21030fadfea320c8e859219019e136f678c6689ed5960b391b822f01d5 - languageName: node - linkType: hard - "safe-stable-stringify@npm:^2.2.0, safe-stable-stringify@npm:^2.3.1": version: 2.3.1 resolution: "safe-stable-stringify@npm:2.3.1" @@ -33450,18 +34489,6 @@ __metadata: languageName: node linkType: hard -"set-value@npm:^2.0.0, set-value@npm:^2.0.1": - version: 2.0.1 - resolution: "set-value@npm:2.0.1" - dependencies: - extend-shallow: ^2.0.1 - is-extendable: ^0.1.1 - is-plain-object: ^2.0.3 - split-string: ^3.0.1 - checksum: 09a4bc72c94641aeae950eb60dc2755943b863780fcc32e441eda964b64df5e3f50603d5ebdd33394ede722528bd55ed43aae26e9df469b4d32e2292b427b601 - languageName: node - linkType: hard - "set-value@npm:^4.1.0": version: 4.1.0 resolution: "set-value@npm:4.1.0" @@ -33671,6 +34698,20 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^14.0.2": + version: 14.0.2 + resolution: "sinon@npm:14.0.2" + dependencies: + "@sinonjs/commons": ^2.0.0 + "@sinonjs/fake-timers": ^9.1.2 + "@sinonjs/samsam": ^7.0.1 + diff: ^5.0.0 + nise: ^5.1.2 + supports-color: ^7.2.0 + checksum: de7730cd7785a457e42f9a93e955780c870296036a13816e3c0c5648360afae82fdc748e36c854cf26fb8abd117855a7211aee49265c334fa61439aae17a1b72 + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -33764,42 +34805,6 @@ __metadata: languageName: node linkType: hard -"snapdragon-node@npm:^2.0.1": - version: 2.1.1 - resolution: "snapdragon-node@npm:2.1.1" - dependencies: - define-property: ^1.0.0 - isobject: ^3.0.0 - snapdragon-util: ^3.0.1 - checksum: 9bb57d759f9e2a27935dbab0e4a790137adebace832b393e350a8bf5db461ee9206bb642d4fe47568ee0b44080479c8b4a9ad0ebe3712422d77edf9992a672fd - languageName: node - linkType: hard - -"snapdragon-util@npm:^3.0.1": - version: 3.0.1 - resolution: "snapdragon-util@npm:3.0.1" - dependencies: - kind-of: ^3.2.0 - checksum: 684997dbe37ec995c03fd3f412fba2b711fc34cb4010452b7eb668be72e8811a86a12938b511e8b19baf853b325178c56d8b78d655305e5cfb0bb8b21677e7b7 - languageName: node - linkType: hard - -"snapdragon@npm:^0.8.1": - version: 0.8.2 - resolution: "snapdragon@npm:0.8.2" - dependencies: - base: ^0.11.1 - debug: ^2.2.0 - define-property: ^0.2.5 - extend-shallow: ^2.0.1 - map-cache: ^0.2.2 - source-map: ^0.5.6 - source-map-resolve: ^0.5.0 - use: ^3.1.0 - checksum: a197f242a8f48b11036563065b2487e9b7068f50a20dd81d9161eca6af422174fc158b8beeadbe59ce5ef172aa5718143312b3aebaae551c124b7824387c8312 - languageName: node - linkType: hard - "socket.io-adapter@npm:~1.1.0": version: 1.1.2 resolution: "socket.io-adapter@npm:1.1.2" @@ -33929,19 +34934,6 @@ __metadata: languageName: node linkType: hard -"source-map-resolve@npm:^0.5.0": - version: 0.5.3 - resolution: "source-map-resolve@npm:0.5.3" - dependencies: - atob: ^2.1.2 - decode-uri-component: ^0.2.0 - resolve-url: ^0.2.1 - source-map-url: ^0.4.0 - urix: ^0.1.0 - checksum: c73fa44ac00783f025f6ad9e038ab1a2e007cd6a6b86f47fe717c3d0765b4a08d264f6966f3bd7cd9dbcd69e4832783d5472e43247775b2a550d6f2155d24bae - languageName: node - linkType: hard - "source-map-support@npm:0.5.13": version: 0.5.13 resolution: "source-map-support@npm:0.5.13" @@ -33962,13 +34954,6 @@ __metadata: languageName: node linkType: hard -"source-map-url@npm:^0.4.0": - version: 0.4.1 - resolution: "source-map-url@npm:0.4.1" - checksum: 64c5c2c77aff815a6e61a4120c309ae4cac01298d9bcbb3deb1b46a4dd4c46d4a1eaeda79ec9f684766ae80e8dc86367b89326ce9dd2b89947bd9291fc1ac08c - languageName: node - linkType: hard - "source-map@npm:0.5.6": version: 0.5.6 resolution: "source-map@npm:0.5.6" @@ -33976,13 +34961,6 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.5.6": - version: 0.5.7 - resolution: "source-map@npm:0.5.7" - checksum: 5dc2043b93d2f194142c7f38f74a24670cd7a0063acdaf4bf01d2964b402257ae843c2a8fa822ad5b71013b5fcafa55af7421383da919752f22ff488bc553f4d - languageName: node - linkType: hard - "source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" @@ -34110,15 +35088,6 @@ __metadata: languageName: node linkType: hard -"split-string@npm:^3.0.1, split-string@npm:^3.0.2": - version: 3.1.0 - resolution: "split-string@npm:3.1.0" - dependencies: - extend-shallow: ^3.0.0 - checksum: ae5af5c91bdc3633628821bde92fdf9492fa0e8a63cf6a0376ed6afde93c701422a1610916f59be61972717070119e848d10dfbbd5024b7729d6a71972d2a84c - languageName: node - linkType: hard - "split2@npm:^3.0.0": version: 3.2.2 resolution: "split2@npm:3.2.2" @@ -34328,16 +35297,6 @@ __metadata: languageName: node linkType: hard -"static-extend@npm:^0.1.1": - version: 0.1.2 - resolution: "static-extend@npm:0.1.2" - dependencies: - define-property: ^0.2.5 - object-copy: ^0.1.0 - checksum: 8657485b831f79e388a437260baf22784540417a9b29e11572c87735df24c22b84eda42107403a64b30861b2faf13df9f7fc5525d51f9d1d2303aba5cbf4e12c - languageName: node - linkType: hard - "statuses@npm:2.0.1, statuses@npm:^2.0.0": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -34359,6 +35318,16 @@ __metadata: languageName: node linkType: hard +"stream-browserify@npm:3.0.0": + version: 3.0.0 + resolution: "stream-browserify@npm:3.0.0" + dependencies: + inherits: ~2.0.4 + readable-stream: ^3.5.0 + checksum: 4c47ef64d6f03815a9ca3874e2319805e8e8a85f3550776c47ce523b6f4c6cd57f40e46ec6a9ab8ad260fde61863c2718f250d3bedb3fe9052444eb9abfd9921 + languageName: node + linkType: hard + "stream-browserify@npm:^2.0.1": version: 2.0.2 resolution: "stream-browserify@npm:2.0.2" @@ -34681,6 +35650,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^1.0.5": + version: 1.0.5 + resolution: "strnum@npm:1.0.5" + checksum: 651b2031db5da1bf4a77fdd2f116a8ac8055157c5420f5569f64879133825915ad461513e7202a16d7fec63c54fd822410d0962f8ca12385c4334891b9ae6dd2 + languageName: node + linkType: hard + "strtok3@npm:^6.2.4": version: 6.2.4 resolution: "strtok3@npm:6.2.4" @@ -35056,7 +36032,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.0.2, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.2": +"tar@npm:^6.0.2, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2": version: 6.1.12 resolution: "tar@npm:6.1.12" dependencies: @@ -35430,25 +36406,6 @@ __metadata: languageName: node linkType: hard -"to-object-path@npm:^0.3.0": - version: 0.3.0 - resolution: "to-object-path@npm:0.3.0" - dependencies: - kind-of: ^3.0.2 - checksum: 9425effee5b43e61d720940fa2b889623f77473d459c2ce3d4a580a4405df4403eec7be6b857455908070566352f9e2417304641ed158dda6f6a365fe3e66d70 - languageName: node - linkType: hard - -"to-regex-range@npm:^2.1.0": - version: 2.1.1 - resolution: "to-regex-range@npm:2.1.1" - dependencies: - is-number: ^3.0.0 - repeat-string: ^1.6.1 - checksum: 46093cc14be2da905cc931e442d280b2e544e2bfdb9a24b3cf821be8d342f804785e5736c108d5be026021a05d7b38144980a61917eee3c88de0a5e710e10320 - languageName: node - linkType: hard - "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -35458,18 +36415,6 @@ __metadata: languageName: node linkType: hard -"to-regex@npm:^3.0.1, to-regex@npm:^3.0.2": - version: 3.0.2 - resolution: "to-regex@npm:3.0.2" - dependencies: - define-property: ^2.0.2 - extend-shallow: ^3.0.2 - regex-not: ^1.0.2 - safe-regex: ^1.1.0 - checksum: 4ed4a619059b64e204aad84e4e5f3ea82d97410988bcece7cf6cbfdbf193d11bff48cf53842d88b8bb00b1bfc0d048f61f20f0709e6f393fd8fe0122662d9db4 - languageName: node - linkType: hard - "toggle-selection@npm:^1.0.6": version: 1.0.6 resolution: "toggle-selection@npm:1.0.6" @@ -35501,10 +36446,10 @@ __metadata: languageName: node linkType: hard -"tosource@npm:^1.0.0": - version: 1.0.0 - resolution: "tosource@npm:1.0.0" - checksum: 683fc64700484cd749b6eed461c2d105e251df7caf886484ff824bcf1249e365210d65c4f2ca3ced30baa34ea7912c656984e084aaada669920b44e2437c1c05 +"tosource@npm:^2.0.0-alpha.3": + version: 2.0.0-alpha.3 + resolution: "tosource@npm:2.0.0-alpha.3" + checksum: bc03a7571de8ed4306e6721283fa891f2adcab9dd80c46f6f177d4259b34bb192fe3a2cb3e1e2ce16f9db0bc7e534acfcb5478ab094b0ba255f98abfce6dab46 languageName: node linkType: hard @@ -35667,7 +36612,7 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:^10.0.0, ts-node@npm:^10.2.1, ts-node@npm:^10.4.0, ts-node@npm:^10.8.1": +"ts-node@npm:^10.0.0, ts-node@npm:^10.4.0, ts-node@npm:^10.8.1, ts-node@npm:^10.9.1": version: 10.9.1 resolution: "ts-node@npm:10.9.1" dependencies: @@ -35731,7 +36676,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.10.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": +"tslib@npm:^1.10.0, tslib@npm:^1.11.1, tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd @@ -35905,25 +36850,25 @@ __metadata: languageName: node linkType: hard -"typescript-json-schema@npm:^0.54.0": - version: 0.54.0 - resolution: "typescript-json-schema@npm:0.54.0" +"typescript-json-schema@npm:^0.55.0": + version: 0.55.0 + resolution: "typescript-json-schema@npm:0.55.0" dependencies: "@types/json-schema": ^7.0.9 "@types/node": ^16.9.2 glob: ^7.1.7 path-equal: ^1.1.2 safe-stable-stringify: ^2.2.0 - ts-node: ^10.2.1 - typescript: ~4.6.0 + ts-node: ^10.9.1 + typescript: ~4.8.2 yargs: ^17.1.1 bin: typescript-json-schema: bin/typescript-json-schema - checksum: 49e03bd2612f79fe3ee9e9afcea34ae563da9aa799a8b4cf12b73feb60eb62a0786300eedb30261c69c482ed7e545acf1e5617d59861e6deee4f6570a658de88 + checksum: 4188e9d4cc1f1fc3201e2aa06b8d87d35695b0eaf049ff9d2cd3027d87406efc6978c4f90c2bf06f4fcd8d6dd40ceb65e744dc3cf9b412e1f74b8eb5bae96a05 languageName: node linkType: hard -"typescript@npm:~4.6.0, typescript@npm:~4.6.3": +"typescript@npm:~4.6.3": version: 4.6.4 resolution: "typescript@npm:4.6.4" bin: @@ -35943,7 +36888,17 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@~4.6.0#~builtin, typescript@patch:typescript@~4.6.3#~builtin": +"typescript@npm:~4.8.2": + version: 4.8.4 + resolution: "typescript@npm:4.8.4" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 + languageName: node + linkType: hard + +"typescript@patch:typescript@~4.6.3#~builtin": version: 4.6.4 resolution: "typescript@patch:typescript@npm%3A4.6.4#~builtin::version=4.6.4&hash=a1c5e5" bin: @@ -35963,6 +36918,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@~4.8.2#~builtin": + version: 4.8.4 + resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=a1c5e5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 563a0ef47abae6df27a9a3ab38f75fc681f633ccf1a3502b1108e252e187787893de689220f4544aaf95a371a4eb3141e4a337deb9895de5ac3c1ca76430e5f0 + languageName: node + linkType: hard + "ua-parser-js@npm:^0.7.18": version: 0.7.28 resolution: "ua-parser-js@npm:0.7.28" @@ -36116,18 +37081,6 @@ __metadata: languageName: node linkType: hard -"union-value@npm:^1.0.0": - version: 1.0.1 - resolution: "union-value@npm:1.0.1" - dependencies: - arr-union: ^3.1.0 - get-value: ^2.0.6 - is-extendable: ^0.1.1 - set-value: ^2.0.1 - checksum: a3464097d3f27f6aa90cf103ed9387541bccfc006517559381a10e0dffa62f465a9d9a09c9b9c3d26d0f4cbe61d4d010e2fbd710fd4bf1267a768ba8a774b0ba - languageName: node - linkType: hard - "unique-filename@npm:^1.1.1": version: 1.1.1 resolution: "unique-filename@npm:1.1.1" @@ -36274,16 +37227,6 @@ __metadata: languageName: node linkType: hard -"unset-value@npm:^1.0.0": - version: 1.0.0 - resolution: "unset-value@npm:1.0.0" - dependencies: - has-value: ^0.3.1 - isobject: ^3.0.0 - checksum: 5990ecf660672be2781fc9fb322543c4aa592b68ed9a3312fa4df0e9ba709d42e823af090fc8f95775b4cd2c9a5169f7388f0cec39238b6d0d55a69fc2ab6b29 - languageName: node - linkType: hard - "untildify@npm:^4.0.0": version: 4.0.0 resolution: "untildify@npm:4.0.0" @@ -36332,13 +37275,6 @@ __metadata: languageName: node linkType: hard -"urix@npm:^0.1.0": - version: 0.1.0 - resolution: "urix@npm:0.1.0" - checksum: 4c076ecfbf3411e888547fe844e52378ab5ada2d2f27625139011eada79925e77f7fbf0e4016d45e6a9e9adb6b7e64981bd49b22700c7c401c5fc15f423303b3 - languageName: node - linkType: hard - "url-parse@npm:^1.5.8": version: 1.5.10 resolution: "url-parse@npm:1.5.10" @@ -36428,13 +37364,6 @@ __metadata: languageName: node linkType: hard -"use@npm:^3.1.0": - version: 3.1.1 - resolution: "use@npm:3.1.1" - checksum: 08a130289f5238fcbf8f59a18951286a6e660d17acccc9d58d9b69dfa0ee19aa038e8f95721b00b432c36d1629a9e32a464bf2e7e0ae6a244c42ddb30bdd8b33 - languageName: node - linkType: hard - "utf8-byte-length@npm:^1.0.1": version: 1.0.4 resolution: "utf8-byte-length@npm:1.0.4" @@ -37846,6 +38775,13 @@ __metadata: languageName: node linkType: hard +"zen-observable@npm:^0.10.0": + version: 0.10.0 + resolution: "zen-observable@npm:0.10.0" + checksum: cee4e8902fcf4ed49f96937e9bc30b980ad3311b85e94b836c77f787163c98f19c65d2d8ac80990e8ecf4c1497d84821d58580d5ee20015f55516de146e8b7af + languageName: node + linkType: hard + "zen-observable@npm:^0.8.15": version: 0.8.15 resolution: "zen-observable@npm:0.8.15"