diff --git a/.changeset/calm-cups-smile.md b/.changeset/calm-cups-smile.md new file mode 100644 index 0000000000..68fcfb2d1a --- /dev/null +++ b/.changeset/calm-cups-smile.md @@ -0,0 +1,60 @@ +--- +'@backstage/plugin-api-docs-module-protoc-gen-doc': patch +'@backstage/plugin-catalog-unprocessed-entities': patch +'@backstage/plugin-github-pull-requests-board': patch +'@backstage/integration-react': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-microsoft-calendar': patch +'@backstage/plugin-newrelic-dashboard': patch +'@backstage/plugin-entity-validation': patch +'@backstage/plugin-apollo-explorer': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-graphql-voyager': patch +'@backstage/plugin-apache-airflow': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-octopus-deploy': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-github-issues': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-code-climate': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-azure-sites': patch +'@backstage/plugin-firehydrant': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-home-react': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-codescene': patch +'@backstage/plugin-dynatrace': patch +'@backstage/plugin-gcalendar': patch +'@backstage/plugin-org-react': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-xcmetrics': patch +'@backstage/plugin-airbrake': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-linguist': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-opencost': patch +'@backstage/plugin-periskop': patch +'@backstage/plugin-playlist': patch +'@backstage/plugin-puppetdb': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bazaar': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-nomad': patch +'@backstage/plugin-vault': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-org': patch +--- + +Moved `@types/react` to be a regular dependency diff --git a/.changeset/chilly-parrots-yell.md b/.changeset/chilly-parrots-yell.md new file mode 100644 index 0000000000..fe4c760923 --- /dev/null +++ b/.changeset/chilly-parrots-yell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Added title to props for `HasCards`. diff --git a/.changeset/cyan-roses-relate.md b/.changeset/cyan-roses-relate.md new file mode 100644 index 0000000000..e6dbeb6ed9 --- /dev/null +++ b/.changeset/cyan-roses-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added the ability to create a plain backend module with the `new` command. diff --git a/.changeset/dirty-carrots-notice.md b/.changeset/dirty-carrots-notice.md new file mode 100644 index 0000000000..ccc31c25ca --- /dev/null +++ b/.changeset/dirty-carrots-notice.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added `authPlugin` export for the new backend system. The plugin does not include any built-in auth providers, they must instead be added by installing additional modules, for example `authModuleGoogleProvider` from `@backstage/plugin-auth-backend-module-google-provider`. diff --git a/.changeset/dull-tables-joke.md b/.changeset/dull-tables-joke.md new file mode 100644 index 0000000000..e987377023 --- /dev/null +++ b/.changeset/dull-tables-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fixed an issue where `EntitySwitch` was preventing the display of entity errors. diff --git a/.changeset/eight-radios-listen.md b/.changeset/eight-radios-listen.md new file mode 100644 index 0000000000..0421a561c0 --- /dev/null +++ b/.changeset/eight-radios-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fixed bug in styles that caused next and previous links in footer to overlap page content. diff --git a/.changeset/fair-paws-flash.md b/.changeset/fair-paws-flash.md new file mode 100644 index 0000000000..d9a7b1827e --- /dev/null +++ b/.changeset/fair-paws-flash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': patch +--- + +Improve consistency of results from the `AzureDevOpsEntityProvider`. diff --git a/.changeset/few-nails-smile.md b/.changeset/few-nails-smile.md new file mode 100644 index 0000000000..2fad9a0b40 --- /dev/null +++ b/.changeset/few-nails-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-entity-feedback': patch +--- + +Improve README to note that Backstage identity is required to be configured diff --git a/.changeset/fresh-candles-warn.md b/.changeset/fresh-candles-warn.md new file mode 100644 index 0000000000..49ca2e5402 --- /dev/null +++ b/.changeset/fresh-candles-warn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-node': patch +--- + +The `BackstageIdentityResponse` interface now has an optional `expiresInSeconds` field that can be used to signal session expiration. The `prepareBackstageIdentityResponse` utility will now also read the expiration from the provided token, and include it in the response. diff --git a/.changeset/great-chairs-swim.md b/.changeset/great-chairs-swim.md new file mode 100644 index 0000000000..e6b8ab0882 --- /dev/null +++ b/.changeset/great-chairs-swim.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +Fixed two bugs in how the `OAuth2Session` type represents the underlying data. The `expiresAt` and `backstageIdentity` are now both optional, since that's what they are in practice. This is not considered a breaking change since it was effectively a bug in the modelling of the state that this type represents, and the type was not used in any other external contract. diff --git a/.changeset/hungry-dots-design.md b/.changeset/hungry-dots-design.md new file mode 100644 index 0000000000..0dd8d415e9 --- /dev/null +++ b/.changeset/hungry-dots-design.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/backend-common': patch +--- + +Use `DefaultAzureDevOpsCredentialsProvider` to retrieve credentials for Azure DevOps. diff --git a/.changeset/late-cows-turn.md b/.changeset/late-cows-turn.md new file mode 100644 index 0000000000..7fa775ce22 --- /dev/null +++ b/.changeset/late-cows-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed a link to the frontend Backstage plugin that had pointed to itself. diff --git a/.changeset/lazy-rice-rule.md b/.changeset/lazy-rice-rule.md new file mode 100644 index 0000000000..1c3a3c3250 --- /dev/null +++ b/.changeset/lazy-rice-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixed the plugin and module ID of the alpha `catalogModuleTemplateKind` export. diff --git a/.changeset/lovely-years-sniff.md b/.changeset/lovely-years-sniff.md new file mode 100644 index 0000000000..09b86f7112 --- /dev/null +++ b/.changeset/lovely-years-sniff.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +The `OAuth` class which is used by all OAuth providers will now consider both the session expiration of both the Backstage identity as well as the upstream identity provider, and refresh the session with either of them is about to expire. diff --git a/.changeset/mean-carpets-provide.md b/.changeset/mean-carpets-provide.md new file mode 100644 index 0000000000..b5e9f551d4 --- /dev/null +++ b/.changeset/mean-carpets-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-azure': patch +--- + +Remove duplications from Azure search before committing the new locations to the catalog. diff --git a/.changeset/mighty-crabs-cheat.md b/.changeset/mighty-crabs-cheat.md new file mode 100644 index 0000000000..ef2abbada4 --- /dev/null +++ b/.changeset/mighty-crabs-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Added the ability to disable the built-in auth providers by passing `disableDefaultProviderFactories` to `createRouter`. diff --git a/.changeset/new-sheep-cover.md b/.changeset/new-sheep-cover.md new file mode 100644 index 0000000000..390de1a9b3 --- /dev/null +++ b/.changeset/new-sheep-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-linguist-backend': patch +--- + +When creating the router using `createRouterFromConfig` or using the plugin for the new backend system the `linguist.useSourceLocation` configuration is now optional. diff --git a/.changeset/nice-forks-remain.md b/.changeset/nice-forks-remain.md new file mode 100644 index 0000000000..6d833ff283 --- /dev/null +++ b/.changeset/nice-forks-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-code-coverage': patch +--- + +Use fetchApi to ensure authorization is used when fetching code-coverage data diff --git a/.changeset/ninety-news-visit.md b/.changeset/ninety-news-visit.md new file mode 100644 index 0000000000..1b990f9ac7 --- /dev/null +++ b/.changeset/ninety-news-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +The algorithm used when generating Backstage tokens can be configured via `auth.identityTokenAlgorithm`. diff --git a/.changeset/old-geese-drive.md b/.changeset/old-geese-drive.md new file mode 100644 index 0000000000..ddaa7ad30a --- /dev/null +++ b/.changeset/old-geese-drive.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Extended `mockService` to also include mocked variants, for example `mockServices.lifecycle.mock()`. The returned mocked implementation will have a `factory` property which is a service factory for itself. You can also pass a partial implementation of the service to the mock function to use a mock implementation of specific methods. diff --git a/.changeset/orange-houses-fly.md b/.changeset/orange-houses-fly.md new file mode 100644 index 0000000000..546484304c --- /dev/null +++ b/.changeset/orange-houses-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Gitiles: Fixed auth prefix issue diff --git a/.changeset/poor-years-appear.md b/.changeset/poor-years-appear.md new file mode 100644 index 0000000000..d5b53a7051 --- /dev/null +++ b/.changeset/poor-years-appear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-entity-feedback-backend': patch +--- + +Improve backend logging if method calls fail diff --git a/.changeset/popular-readers-allow.md b/.changeset/popular-readers-allow.md new file mode 100644 index 0000000000..b89e0c264e --- /dev/null +++ b/.changeset/popular-readers-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Added the optional `expiresAt` field that may now be part of a `BackstageIdentityResponse`. diff --git a/.changeset/repo-p-lisa.md b/.changeset/repo-p-lisa.md new file mode 100644 index 0000000000..d6a0fa56f4 --- /dev/null +++ b/.changeset/repo-p-lisa.md @@ -0,0 +1,7 @@ +--- +'@backstage/repo-tools': patch +--- + +The `generate-catalog-info` command now uses the first listed `CODEOWNER` as Component owner when initially +creating the `catalog-info.yaml` file. It continues to allow any one listed `CODEOWNER` when updating +entity metadata. diff --git a/.changeset/repo-tools-boo-descriptions.md b/.changeset/repo-tools-boo-descriptions.md new file mode 100644 index 0000000000..5973dd7127 --- /dev/null +++ b/.changeset/repo-tools-boo-descriptions.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Fixed a bug with the `generate-catalog-info` command that could cause the `--dry-run` flag to indicate changes to files when no changes would actually be made if the command were run without the flag. diff --git a/.changeset/repo-tools-ci-toldya-so.md b/.changeset/repo-tools-ci-toldya-so.md new file mode 100644 index 0000000000..20425fc631 --- /dev/null +++ b/.changeset/repo-tools-ci-toldya-so.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Added a `--ci` flag to the `generate-catalog-info` command. This flag behaves similarly to the same flag on `api-reports`: if `catalog-info.yaml` files would have been added or modified, then the process exits with status code `1`, and instructions are printed. diff --git a/.changeset/six-wolves-admire.md b/.changeset/six-wolves-admire.md new file mode 100644 index 0000000000..efc80826c2 --- /dev/null +++ b/.changeset/six-wolves-admire.md @@ -0,0 +1,23 @@ +--- +'@backstage/integration': minor +--- + +Added `AzureDevOpsCredentialsProvider` to support multiple Azure DevOps organizations and **deprecated** `AzureIntegrationConfig.credential` and `AzureIntegrationConfig.token` in favour of `AzureIntegrationConfig.credentials`. You can now use specific credentials for different Azure DevOps (Server) organizations by specifying the `organizations` field on a credential: + +```yaml +integrations: + azure: + - host: dev.azure.com + credentials: + - organizations: + - my-org + - my-other-org + clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} + - organizations: + - yet-another-org + personalAccessToken: ${PERSONAL_ACCESS_TOKEN} +``` + +See the [Azure integration documentation](https://backstage.io/docs/integrations/azure/locations) for more information. diff --git a/.changeset/slow-ducks-hunt.md b/.changeset/slow-ducks-hunt.md new file mode 100644 index 0000000000..0aac3056cf --- /dev/null +++ b/.changeset/slow-ducks-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added support for the `dev/index` entry point for backend plugins and modules. diff --git a/.changeset/smart-islands-jog.md b/.changeset/smart-islands-jog.md new file mode 100644 index 0000000000..50bc36191f --- /dev/null +++ b/.changeset/smart-islands-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fix OpenAPI schema for the facets endpoint diff --git a/.changeset/strange-frogs-count.md b/.changeset/strange-frogs-count.md new file mode 100644 index 0000000000..94fa6b9fb6 --- /dev/null +++ b/.changeset/strange-frogs-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add loading indicator to Table diff --git a/.changeset/warm-llamas-bake.md b/.changeset/warm-llamas-bake.md new file mode 100644 index 0000000000..3da4a9b94d --- /dev/null +++ b/.changeset/warm-llamas-bake.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Do not unnecessarily notify subscribers when no-op updates to config happen diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5c957807cf..722c30011d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,7 +5,9 @@ # https://help.github.com/articles/about-codeowners/ * @backstage/maintainers +yarn.lock @backstage/maintainers @backstage-service */yarn.lock @backstage/maintainers @backstage-service +/.changeset/*.md /cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-maintainers /docs/assets/search @backstage/discoverability-maintainers /docs/features/search @backstage/discoverability-maintainers @@ -91,4 +93,3 @@ /tech-insights-backend-module-jsonfc @backstage/maintainers @xantier @iain-b /tech-insights-tech-insights-common @backstage/maintainers @xantier @iain-b /tech-insights-tech-insights-node @backstage/maintainers @xantier @iain-b -yarn.lock @backstage/maintainers @backstage-service diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index 22421925c7..6d662ea05f 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -27,3 +27,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} HUSKY: '0' + + - name: Discord notification + if: ${{ failure() }} + uses: Ilshidur/action-discord@0.3.2 + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} + with: + args: 'Version Packages Sync Failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}' diff --git a/OWNERS.md b/OWNERS.md index 66575e8e35..85f7fe545a 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -90,10 +90,15 @@ Team: @backstage/techdocs-maintainers Scope: The TechDocs plugin and related tooling -| Name | Organization | Team | GitHub | Discord | -| ------------------------ | ------------ | ------------ | ------------------------------------------------ | ------------------ | -| Morgan Bentell | Spotify | Pulp Fiction | [agentbellnorm](http://github.com/agentbellnorm) | morganbentell#9030 | -| Raghunandan Balachandran | Spotify | Pulp Fiction | [soapraj](http://github.com/soapraj) | raghunandanb#1114 | +| Name | Organization | Team | GitHub | Discord | +| ------------------------ | ------------ | ------- | ------------------------------------------------- | ------------------- | +| Alex Lorenzi | Spotify | ProTean | [AlexLorenzi](https://github.com/AlexLorenzi) | `alexlorenzi` | +| Bond Yan | Spotify | ProTean | [byan1197](https://github.com/byan1197) | `bondify` | +| Jackson Chen | Spotify | ProTean | [PeaWarrior](https://github.com/PeaWarrior) | `jacksonc#3322` | +| John Philip | Spotify | ProTean | [johnphilip283](https://github.com/johnphilip283) | `john_philip#2399` | +| Morgan Bentell | Spotify | ProTean | [agentbellnorm](http://github.com/agentbellnorm) | `morganbentell` | +| Raghunandan Balachandran | Spotify | BUX | [soapraj](http://github.com/soapraj) | `raghunandanb#1114` | +| Sydney Achinger | Spotify | ProTean | [squid-ney](https://github.com/squid-ney) | - | ## Incubating Project Areas diff --git a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md index f28a62c018..b88efbb561 100644 --- a/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md +++ b/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md @@ -29,7 +29,7 @@ In the AWS console, configure ALB Authentication: - Select `OIDC` under `Authenticate` - Set `Issuer` to `https://login.microsoftonline.com/{TENANT_ID}/v2.0`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. - Set `Authorization endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. -- Set `Token endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/authorize`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. +- Set `Token endpoint` to `https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token`, replacing `{TENANT_ID}` with the value of `Directory (tenant) ID` of the AAD App. - Set `User info endpoint` to `https://graph.microsoft.com/oidc/userinfo` - Set `Client ID` to the AAD App `Application (client) Id` - Set `Client secret` to the AAD APP `client secret` diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 644816b053..750e18baf5 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -85,6 +85,8 @@ declare a dependency on the plugin package itself. This is to avoid a direct dependency and potentially cause duplicate installations of the plugin package, while duplicate installations of library packages should always be supported. +To create a Backend module, run `yarn new`, select `backend-module`, and fill out the rest of the prompts. This will create a new package at `plugins/-backend-module-`. + The following is an example of how to create a module that adds a new processor using the `catalogProcessingExtensionPoint`: diff --git a/docs/features/software-catalog/references.md b/docs/features/software-catalog/references.md index 6e0304b2e8..2240d376ce 100644 --- a/docs/features/software-catalog/references.md +++ b/docs/features/software-catalog/references.md @@ -23,9 +23,9 @@ compound reference structure. ## String References -This is the most common alternative, that is used in almost all circumstances. +This is the most common alternative and is used in almost all circumstances. -The string is on the form `[:][/]`, that is, it is +The string is of the form `[:][/]`. That is, it is composed of between one and three parts in this specific order, without any additional encoding: diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md index 5647a6cba4..7553df4081 100644 --- a/docs/integrations/azure/locations.md +++ b/docs/integrations/azure/locations.md @@ -19,10 +19,10 @@ Using a service principal: integrations: azure: - host: dev.azure.com - credential: - clientId: ${CLIENT_ID} - clientSecret: ${CLIENT_SECRET} - tenantId: ${TENANT_ID} + credentials: + - clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} ``` Using a managed identity: @@ -31,8 +31,8 @@ Using a managed identity: integrations: azure: - host: dev.azure.com - credential: - clientId: ${CLIENT_ID} + credentials: + - clientId: ${AZURE_CLIENT_ID} ``` Using a personal access token (PAT): @@ -41,24 +41,54 @@ Using a personal access token (PAT): integrations: azure: - host: dev.azure.com - token: ${AZURE_TOKEN} + credentials: + - personalAccessToken: ${PERSONAL_ACCESS_TOKEN} ``` +You can use specific credentials for different Azure DevOps organizations by specifying the `organizations` field on the credential: + +```yaml +integrations: + azure: + - host: dev.azure.com + credentials: + - organizations: + - my-org + - my-other-org + clientId: ${AZURE_CLIENT_ID} + clientSecret: ${AZURE_CLIENT_SECRET} + tenantId: ${AZURE_TENANT_ID} + - organizations: + - another-org + clientId: ${AZURE_CLIENT_ID} + - organizations: + - yet-another-org + personalAccessToken: ${PERSONAL_ACCESS_TOKEN} +``` + +If you do not specify the `organizations` field the credential will be used for all organizations for which no other credential is configured. + > Note: An Azure DevOps provider is added automatically at startup for > convenience, so you only need to list it if you want to supply a -> [token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate), +> [personalAccessToken](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate), > a [service principal](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity), > or a [managed identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity) The configuration is a structure with these elements: -- `host`: The DevOps host; only `dev.azure.com` is supported. -- `token` (optional): A personal access token as expected by Azure DevOps. -- `credential`: (optional): A service principal or managed identity +- `credentials`: (optional): A service principal, managed identity, or personal access token + +The `credentials` element is a structure with these elements: + +- `organizations`: (optional): A list of organizations for which this credential should be used. If not specified the credential will be used for all organizations for which no other credential is configured. +- `clientId`: The client ID of the service principal or managed identity (required for service principal and managed identities) +- `clientSecret`: The client secret of the service principal (required for service principal) +- `tenantId`: The tenant ID of the service principal (required for service principal) +- `personalAccessToken`: The personal access token (required for personal access token) > Note: > -> - `token` and `credential` are mutually exclusive. > - You cannot use a service principal or managed identity for Azure DevOps Server (on-premises) organizations > - You can only use a service principal or managed identity for Azure AD backed Azure DevOps organizations -> - The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API +> - You can only specify one credential per host without any organizations specified +> - The personal access token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API diff --git a/docs/publishing.md b/docs/publishing.md index 12e4878d5a..b2224e76b7 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -18,6 +18,22 @@ PR is merged. This is typically done every Tuesday around noon CET. **This emergency release process is intended only for the Backstage maintainers.** +Given one or more PRs towards master that we want to create a patch release for, run the following script from the repo root: + +```bash +./scripts/patch-release-for-pr.js ... +``` + +Wait until the script has finished executing, at the end of the output you will find a link of the format `https://github.com/backstage/backstage/compare/patch/...`. Open this link in your browser to create a PR for the patch release. Finish the sentence "This release fixes an issue where..." and create the PR. + +Once the PR has been approved and merged, the patch release will be automatically created. The patch release is complete when a notification has been posted to Discord in the `#announcements` channel. Keep an eye on "Deploy Packages" workflow and re-trigger if it fails. It is safe to re-trigger any part of this workflow, including the release step. + +If the above process fails, you can fall back to the manual process documented below. + +### Old Process + +This is the old and manual process that we used before the patch script, provided here as a reference: + For this example we will be using the `@backstage/plugin-foo` package as an example and assume that it is currently version `6.5.0` in the master branch. diff --git a/microsite/blog/2023-08-17-expedia-proof-of-value-metrics-2.mdx b/microsite/blog/2023-08-17-expedia-proof-of-value-metrics-2.mdx new file mode 100644 index 0000000000..9c6e67fcf1 --- /dev/null +++ b/microsite/blog/2023-08-17-expedia-proof-of-value-metrics-2.mdx @@ -0,0 +1,206 @@ +--- +# prettier-ignore +title: "Expedia Group shares Backstage proof of value metrics 2.0" +author: Guillermo Manzo, Expedia Group +authorURL: https://www.linkedin.com/in/guillermomanzo +--- + +![backstage header](assets/2023-08-17/header.png) + +_Editor's Note: In this post, Expedia Group provides a follow-up to their initial Q4 2021 [post on how they measure Backstage proof of value](https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/). +We're grateful to the Expedia Group team for sharing their processes, revised proof of value metrics, and adaptations to their approach over time in hopes of providing a successful framework for the rest of the Backstage community._ + +[Expedia Group's](https://expediagroup.com) mission is to power global travel for everyone, everywhere. +This vision is made possible through Expedia Group's massive platform, which connects 168 million loyalty members, 50 thousand B2B partners, 3 million properties and 500 airlines, car rentals and cruise lines across the globe. In short, the scale of our platform is enormous, which means that we need to scale our engineering capabilities as well so we can continue to provide seamless experiences to our travelers and partners. + +{/* truncate */} + +In August 2020, we adopted Backstage to enhance our foundational platform capabilities and reduce developer friction in the process. Its framework proved scalable for us and as EG's platform evolved, so did our application of Backstage. Since our [initial share about our Backstage deployment](https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/), we've continued to follow our process of: + +- Syncing Expedia Group's internal Developer Journey phases by key DevOps processes and Software Delivery Life Cycle (SDLC) milestones (POC, Alpha, Beta, etc). +- Identifying Developer Jobs to be Done (JTBD) by our Internal Developer Journey phases +- Aligning developer JTBD's to proof of value (PoV) metrics. + +In the diagram below, you can see how we distilled these processes into Backstage PoV metrics. + +![backstage pov metrics](assets/2023-08-17/table.png) + +To ground ourselves in the evolving developer and Backstage landscape at Expedia Group, we constantly ask ourselves, "How can we continue to show the value of Backstage to our customers and the Developer Community?" To fully answer this question, we adapted our initial proof of value metrics to reinforce our alignment to the current landscape of the Expedia Group developer journey. + +## Expedia Group proof of value metrics 2.0 + +We are excited to share our PoV metrics because it affirms our investment in Backstage for Expedia Group's Backstage users and provides a compelling metrics-based answer on why more organizations should consider adopting it. + +As our Backstage application matured, we updated our PoV metrics and aligned our 2023 roadmap to reflect these changes. Below, we highlight key wins from 2022 and outline our long-term vision for each metric. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PoV MetricDescription2022 Key Win(s)Long-Term Vision
"Ship on Day One" / onboarding metrics +

+ A metric used to determine an engineer's readiness to develop at EG + during their onboarding process. +

+

+ It measures the time from first commit to deploying code in + production, using a "Hello World Template" +

+
+
    +
  • + 850+ engineers went through the “Ship on Day One” Bootcamp when + first joining Expedia Group +
  • +
  • + We had a success rate of ~95% of engineers completing bootcamp on + first try +
  • +
  • 7,000+ components created or onboarded
  • +
  • 4-min average per app creation/onboarding
  • +
+
+
    +
  • + Develop full onboarding curriculum coverage for all critical + capabilities offered within EG - Example: performance and quality + management, pipeline management, etc. +
  • +
  • + Create an end-to-end guided tour of the EG Developer Journey w/ + functional training +
  • +
+
+ Reduce context switching through integration with core EG capabilities + + Through plugins, tool integration, reduces context-switching as + engineers will not need to toggle between multiple tools + +
    +
  • + 15%+ of internal developer tools are integrated within Backstage, + reducing the amount of context-switching needed +
  • +
  • + 4000+ unique users using Backstage for ~20 min on average a day. +
  • +
+
+
    +
  • + End-to-end integration with EG paved road tools and Backstage to + support increased developer productivity +
  • +
  • + Drive broader adoption to have all EG employees using Backstage to + manage their SDLC and to build internal UIs using plugins +
  • +
+
Improve search results relevance + Intuitive search capabilities to surface relevant Backstage content like + TechDocs, entities, tools, etc. + +
    +
  • Enabled global search functionality throughout Backstage
  • +
  • +

    Contributed to the open source Backstage Search platform

    +
  • +
+
+
    +
  • Engine optimization/ranking improvement
  • +
  • Visualize key search metrics to support improvements
  • +
  • Chat + AI integration with chat-based interfaces
  • +
+
Technical documentation reach + Number of developers leveraging TechDocs for all services within EG + +
    +
  • 500+ services tied with TechDocs
  • +
  • 50,000+ average monthly TechDoc views
  • +
+
+
    +
  • + All Backstage entities leveraging TechDocs to enable a centralized + documentation portal integrated with the Software Catalog +
  • +
  • + Quality/governance and gamification to support users and + contributors +
  • +
  • + More TechDocs and mkdocs add-ons to compliment the core capabilities + and drive usability of documentation on Backstage +
  • +
+
Developer Experience community engagementEnhance Backstage through community engagement +
    +
  • + Diverse contributions to Backstage open source focused on search and + discovery - 10+ +
  • +
  • Community engagement through internal case studies, and blogs
  • +
+
+
    +
  • + Continuous contributions to open-source Backstage, focused on plugin + administration, driving documentation adoption, and improved UX + around core capabilities +
  • +
  • + Strategic community engagements to enhance the platform's usability + and long-term confidence in adopting Backstage +
  • +
  • + Event participation and storytelling around EG's Backstage journey +
  • +
+
+ +## YMMV: Your Metrics May Vary! + +Measuring the value of Backstage will vary depending on your organization's needs and priorities. As an early adopter of Backstage, we hope the Expedia Group PoV metrics provide a helpful framework for understanding how Backstage can be used to improve developer productivity, collaboration, and knowledge sharing. + +We look forward to sharing future updates with the community as we continue to embrace a continuous improvement approach to Backstage. If you missed it, you can read about where we started in our [initial proof of value metrics](https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/) back in 2021. + +_Read more about Expedia's tech journey on [their Medium Blog](https://medium.com/expedia-group-tech)_ diff --git a/microsite/blog/assets/2023-08-17/header.png b/microsite/blog/assets/2023-08-17/header.png new file mode 100644 index 0000000000..bd75529f72 Binary files /dev/null and b/microsite/blog/assets/2023-08-17/header.png differ diff --git a/microsite/blog/assets/2023-08-17/table.png b/microsite/blog/assets/2023-08-17/table.png new file mode 100644 index 0000000000..c832740634 Binary files /dev/null and b/microsite/blog/assets/2023-08-17/table.png differ diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index f8e535c8a7..f0fa15efb0 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -9,6 +9,7 @@ import { AppConfig } from '@backstage/config'; import { AwsCredentialsManager } from '@backstage/integration-aws-node'; import { AwsS3Integration } from '@backstage/integration'; +import { AzureDevOpsCredentialsProvider } from '@backstage/integration'; import { AzureIntegration } from '@backstage/integration'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketCloudIntegration } from '@backstage/integration'; @@ -94,6 +95,7 @@ export class AzureUrlReader implements UrlReader { integration: AzureIntegration, deps: { treeResponseFactory: ReadTreeResponseFactory; + credentialsProvider: AzureDevOpsCredentialsProvider; }, ); // (undocumented) diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index f969b3b7eb..6b2f41a2bf 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -17,7 +17,11 @@ import { ConfigReader } from '@backstage/config'; import { AzureIntegration, + DefaultAzureDevOpsCredentialsProvider, readAzureIntegrationConfig, + ScmIntegrations, + AzureDevOpsCredentialLike, + AzureIntegrationConfig, } from '@backstage/integration'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -31,12 +35,40 @@ import { getVoidLogger } from '../logging'; import { AzureUrlReader } from './AzureUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; +type AzureIntegrationConfigLike = Partial< + Omit +> & { + credentials?: Partial[]; +}; + const logger = getVoidLogger(); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }); +const urlReaderFactory = (azureIntegration: AzureIntegrationConfigLike) => { + const credentialsProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations( + ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [azureIntegration], + }, + }), + ), + ); + return new AzureUrlReader( + new AzureIntegration( + readAzureIntegrationConfig(new ConfigReader(azureIntegration)), + ), + { + treeResponseFactory, + credentialsProvider, + }, + ); +}; + const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; describe('AzureUrlReader', () => { @@ -68,13 +100,29 @@ describe('AzureUrlReader', () => { ); }); - const createConfig = (token?: string) => - new ConfigReader( + const createConfig = (token?: string) => { + let credentials: AzureDevOpsCredentialLike[] | undefined = undefined; + if (token !== undefined) { + credentials = [ + { + personalAccessToken: token, + }, + ]; + } + return new ConfigReader( { - integrations: { azure: [{ host: 'dev.azure.com', token }] }, + integrations: { + azure: [ + { + host: 'dev.azure.com', + credentials: credentials, + }, + ], + }, }, 'test-config', ); + }; it.each([ { @@ -104,8 +152,8 @@ describe('AzureUrlReader', () => { url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml', config: createConfig(undefined), response: expect.objectContaining({ - headers: expect.not.objectContaining({ - authorization: expect.anything(), + headers: expect.objectContaining({ + authorization: expect.stringMatching(/^Bearer /), }), }), }, @@ -137,7 +185,7 @@ describe('AzureUrlReader', () => { url: '', config: createConfig(''), error: - "Invalid type in config for key 'integrations.azure[0].token' in 'test-config', got empty-string, wanted string", + "Invalid type in config for key 'integrations.azure[0].credentials[0].personalAccessToken' in 'test-config', got empty-string, wanted string", }, ])('should handle error path %#', async ({ url, config, error }) => { await expect(async () => { @@ -156,16 +204,14 @@ describe('AzureUrlReader', () => { path.resolve(__dirname, '__fixtures__/mock-main.zip'), ); - const processor = new AzureUrlReader( - new AzureIntegration( - readAzureIntegrationConfig( - new ConfigReader({ - host: 'dev.azure.com', - }), - ), - ), - { treeResponseFactory }, - ); + const processor = urlReaderFactory({ + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'my-pat', + }, + ], + }); beforeEach(() => { worker.use( @@ -268,16 +314,14 @@ describe('AzureUrlReader', () => { path.resolve(__dirname, '__fixtures__/mock-main.zip'), ); - const processor = new AzureUrlReader( - new AzureIntegration( - readAzureIntegrationConfig( - new ConfigReader({ - host: 'dev.azure.com', - }), - ), - ), - { treeResponseFactory }, - ); + const processor = urlReaderFactory({ + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'my-pat', + }, + ], + }); beforeEach(() => { worker.use( diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index 94abf3a178..783736b639 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -15,12 +15,13 @@ */ import { - AzureIntegration, getAzureCommitsUrl, getAzureDownloadUrl, getAzureFileFetchUrl, - getAzureRequestOptions, + AzureDevOpsCredentialsProvider, + DefaultAzureDevOpsCredentialsProvider, ScmIntegrations, + AzureIntegration, } from '@backstage/integration'; import fetch, { Response } from 'node-fetch'; import { Minimatch } from 'minimatch'; @@ -47,8 +48,13 @@ import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; export class AzureUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const integrations = ScmIntegrations.fromConfig(config); + const credentialProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(integrations); return integrations.azure.list().map(integration => { - const reader = new AzureUrlReader(integration, { treeResponseFactory }); + const reader = new AzureUrlReader(integration, { + treeResponseFactory, + credentialsProvider: credentialProvider, + }); const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); @@ -56,7 +62,10 @@ export class AzureUrlReader implements UrlReader { constructor( private readonly integration: AzureIntegration, - private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, + private readonly deps: { + treeResponseFactory: ReadTreeResponseFactory; + credentialsProvider: AzureDevOpsCredentialsProvider; + }, ) {} async read(url: string): Promise { @@ -72,11 +81,13 @@ export class AzureUrlReader implements UrlReader { const { signal } = options ?? {}; const builtUrl = getAzureFileFetchUrl(url); - let response: Response; try { + const credentials = await this.deps.credentialsProvider.getCredentials({ + url: builtUrl, + }); response = await fetch(builtUrl, { - ...(await getAzureRequestOptions(this.integration.config)), + headers: credentials?.headers, // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can @@ -111,10 +122,13 @@ export class AzureUrlReader implements UrlReader { // Get latest commit SHA - const commitsAzureResponse = await fetch( - getAzureCommitsUrl(url), - await getAzureRequestOptions(this.integration.config), - ); + const credentials = await this.deps.credentialsProvider.getCredentials({ + url: url, + }); + + const commitsAzureResponse = await fetch(getAzureCommitsUrl(url), { + headers: credentials?.headers, + }); if (!commitsAzureResponse.ok) { const message = `Failed to read tree from ${url}, ${commitsAzureResponse.status} ${commitsAzureResponse.statusText}`; if (commitsAzureResponse.status === 404) { @@ -129,9 +143,10 @@ export class AzureUrlReader implements UrlReader { } const archiveAzureResponse = await fetch(getAzureDownloadUrl(url), { - ...(await getAzureRequestOptions(this.integration.config, { + headers: { + ...credentials?.headers, Accept: 'application/zip', - })), + }, // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can be @@ -198,7 +213,9 @@ export class AzureUrlReader implements UrlReader { } toString() { - const { host, token } = this.integration.config; - return `azure{host=${host},authed=${Boolean(token)}}`; + const { host, credentials } = this.integration.config; + return `azure{host=${host},authed=${Boolean( + credentials !== undefined && credentials.length > 0, + )}}`; } } diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index d086b1d2ae..95f67da35f 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { CacheService } from '@backstage/backend-plugin-api'; @@ -19,6 +21,7 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; +import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; @@ -33,11 +36,19 @@ export namespace mockServices { export namespace cache { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace database { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace httpRouter { @@ -45,6 +56,10 @@ export namespace mockServices { factory: ( options?: HttpRouterFactoryOptions | undefined, ) => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function identity(): IdentityService; @@ -52,21 +67,37 @@ export namespace mockServices { export namespace identity { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace lifecycle { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace logger { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace permissions { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function rootConfig(options?: rootConfig.Options): RootConfigService; @@ -85,6 +116,10 @@ export namespace mockServices { export namespace rootLifecycle { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function rootLogger(options?: rootLogger.Options): LoggerService; @@ -98,11 +133,19 @@ export namespace mockServices { factory: ( options?: Options | undefined, ) => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace scheduler { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export function tokenManager(): TokenManagerService; @@ -110,14 +153,34 @@ export namespace mockServices { export namespace tokenManager { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace urlReader { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } } +// @public (undocumented) +export type ServiceMock = { + factory: ServiceFactory; +} & { + [Key in keyof TService]: TService[Key] extends ( + this: infer This, + ...args: infer Args + ) => infer Return + ? TService[Key] & jest.MockInstance + : TService[Key]; +}; + // @public export function setupRequestMockHandlers(worker: { listen: (t: any) => void; diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 02cb1d5cf9..21946be36c 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -57,6 +57,9 @@ "testcontainers": "^8.1.2", "uuid": "^8.0.0" }, + "peerDependencies": { + "@types/jest": "*" + }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", diff --git a/packages/backend-test-utils/src/next/services/index.ts b/packages/backend-test-utils/src/next/services/index.ts index f562150efa..97a22567fd 100644 --- a/packages/backend-test-utils/src/next/services/index.ts +++ b/packages/backend-test-utils/src/next/services/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { mockServices } from './mockServices'; +export { mockServices, type ServiceMock } from './mockServices'; diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 0455b9f1ae..3edc3e0894 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -40,6 +40,7 @@ import { JsonObject } from '@backstage/types'; import { MockIdentityService } from './MockIdentityService'; import { MockRootLoggerService } from './MockRootLoggerService'; +/** @internal */ function simpleFactory< TService, TScope extends 'root' | 'plugin', @@ -57,6 +58,44 @@ function simpleFactory< })) as (...options: TOptions) => ServiceFactory; } +/** @public */ +export type ServiceMock = { + factory: ServiceFactory; +} & { + [Key in keyof TService]: TService[Key] extends ( + this: infer This, + ...args: infer Args + ) => infer Return + ? TService[Key] & jest.MockInstance + : TService[Key]; +}; + +/** @internal */ +function simpleMock( + ref: ServiceRef, + mockFactory: () => jest.Mocked, +): (partialImpl?: Partial) => ServiceMock { + return partialImpl => { + const mock = mockFactory(); + if (partialImpl) { + for (const [key, impl] of Object.entries(partialImpl)) { + if (typeof impl === 'function') { + (mock as any)[key].mockImplementation(impl); + } else { + (mock as any)[key] = impl; + } + } + } + return Object.assign(mock, { + factory: createServiceFactory({ + service: ref, + deps: {}, + factory: () => mock, + })(), + }) as ServiceMock; + }; +} + /** * @public */ @@ -79,6 +118,13 @@ export namespace mockServices { }; export const factory = simpleFactory(coreServices.rootLogger, rootLogger); + export const mock = simpleMock(coreServices.rootLogger, () => ({ + child: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + })); } export function tokenManager(): TokenManagerService { @@ -98,6 +144,10 @@ export namespace mockServices { coreServices.tokenManager, tokenManager, ); + export const mock = simpleMock(coreServices.tokenManager, () => ({ + authenticate: jest.fn(), + getToken: jest.fn(), + })); } export function identity(): IdentityService { @@ -105,6 +155,9 @@ export namespace mockServices { } export namespace identity { export const factory = simpleFactory(coreServices.identity, identity); + export const mock = simpleMock(coreServices.identity, () => ({ + getIdentity: jest.fn(), + })); } // TODO(Rugvip): Not all core services have implementations available here yet. @@ -112,29 +165,71 @@ export namespace mockServices { // re-implement functioning mock versions here. export namespace cache { export const factory = cacheServiceFactory; + export const mock = simpleMock(coreServices.cache, () => ({ + delete: jest.fn(), + get: jest.fn(), + set: jest.fn(), + withOptions: jest.fn(), + })); } export namespace database { export const factory = databaseServiceFactory; + export const mock = simpleMock(coreServices.database, () => ({ + getClient: jest.fn(), + })); } export namespace httpRouter { export const factory = httpRouterServiceFactory; + export const mock = simpleMock(coreServices.httpRouter, () => ({ + use: jest.fn(), + })); } export namespace lifecycle { export const factory = lifecycleServiceFactory; + export const mock = simpleMock(coreServices.lifecycle, () => ({ + addShutdownHook: jest.fn(), + addStartupHook: jest.fn(), + })); } export namespace logger { export const factory = loggerServiceFactory; + export const mock = simpleMock(coreServices.logger, () => ({ + child: jest.fn(), + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + })); } export namespace permissions { export const factory = permissionsServiceFactory; + export const mock = simpleMock(coreServices.permissions, () => ({ + authorize: jest.fn(), + authorizeConditional: jest.fn(), + })); } export namespace rootLifecycle { export const factory = rootLifecycleServiceFactory; + export const mock = simpleMock(coreServices.rootLifecycle, () => ({ + addShutdownHook: jest.fn(), + addStartupHook: jest.fn(), + })); } export namespace scheduler { export const factory = schedulerServiceFactory; + export const mock = simpleMock(coreServices.scheduler, () => ({ + createScheduledTaskRunner: jest.fn(), + getScheduledTasks: jest.fn(), + scheduleTask: jest.fn(), + triggerTask: jest.fn(), + })); } export namespace urlReader { export const factory = urlReaderServiceFactory; + export const mock = simpleMock(coreServices.urlReader, () => ({ + readTree: jest.fn(), + readUrl: jest.fn(), + search: jest.fn(), + })); } } diff --git a/packages/cli/package.json b/packages/cli/package.json index 95095fe78c..082a068fbd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -140,6 +140,8 @@ }, "devDependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", "@backstage/config": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/core-components": "workspace:^", diff --git a/packages/cli/src/commands/start/startBackend.ts b/packages/cli/src/commands/start/startBackend.ts index 3e6a13be96..622eeb9d1b 100644 --- a/packages/cli/src/commands/start/startBackend.ts +++ b/packages/cli/src/commands/start/startBackend.ts @@ -26,7 +26,17 @@ interface StartBackendOptions { } export async function startBackend(options: StartBackendOptions) { - if (process.env.EXPERIMENTAL_BACKEND_START) { + const hasDev = await fs.pathExists(paths.resolveTarget('dev')); + if (hasDev) { + const waitForExit = await startBackendExperimental({ + entry: 'dev/index', + checksEnabled: false, // not supported + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); + + await waitForExit(); + } else if (process.env.EXPERIMENTAL_BACKEND_START) { const waitForExit = await startBackendExperimental({ entry: 'src/index', checksEnabled: false, // not supported diff --git a/packages/cli/src/lib/new/factories/backendModule.test.ts b/packages/cli/src/lib/new/factories/backendModule.test.ts new file mode 100644 index 0000000000..c633b1c1b2 --- /dev/null +++ b/packages/cli/src/lib/new/factories/backendModule.test.ts @@ -0,0 +1,121 @@ +/* + * 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 fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { sep, resolve as resolvePath } from 'path'; +import { paths } from '../../paths'; +import { Task } from '../../tasks'; +import { FactoryRegistry } from '../FactoryRegistry'; +import { createMockOutputStream, mockPaths } from './common/testUtils'; +import { backendModule } from './backendModule'; + +describe('backendModule factory', () => { + beforeEach(() => { + mockPaths({ + targetRoot: '/root', + }); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should create a backend plugin', async () => { + mockFs({ + '/root': { + packages: { + backend: { + 'package.json': JSON.stringify({}), + }, + }, + plugins: mockFs.directory(), + }, + [paths.resolveOwn('templates')]: mockFs.load( + paths.resolveOwn('templates'), + ), + }); + + const options = await FactoryRegistry.populateOptions(backendModule, { + id: 'test', + moduleId: 'tester-two', + }); + + let modified = false; + + const [output, mockStream] = createMockOutputStream(); + jest.spyOn(process, 'stderr', 'get').mockReturnValue(mockStream); + jest.spyOn(Task, 'forCommand').mockResolvedValue(); + + await backendModule.create(options, { + private: true, + isMonoRepo: true, + defaultVersion: '1.0.0', + markAsModified: () => { + modified = true; + }, + createTemporaryDirectory: () => fs.mkdtemp('test'), + }); + + expect(modified).toBe(true); + + expect(output).toEqual([ + '', + 'Creating backend module backstage-plugin-test-backend-module-tester-two', + 'Checking Prerequisites:', + `availability plugins${sep}test-backend-module-tester-two`, + 'creating temp dir', + 'Executing Template:', + 'copying .eslintrc.js', + 'templating README.md.hbs', + 'templating package.json.hbs', + 'templating index.ts.hbs', + 'templating module.ts.hbs', + 'Installing:', + `moving plugins${sep}test-backend-module-tester-two`, + 'backend adding dependency', + ]); + + await expect( + fs.readJson('/root/packages/backend/package.json'), + ).resolves.toEqual({ + dependencies: { + 'backstage-plugin-test-backend-module-tester-two': '^1.0.0', + }, + }); + const moduleFile = await fs.readFile( + '/root/plugins/test-backend-module-tester-two/src/module.ts', + 'utf-8', + ); + + expect(moduleFile).toContain( + `const testModuleTesterTwo = createBackendModule({`, + ); + expect(moduleFile).toContain(`pluginId: 'test',`); + expect(moduleFile).toContain(`moduleId: 'tester-two',`); + + expect(Task.forCommand).toHaveBeenCalledTimes(2); + expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { + cwd: resolvePath('/root/plugins/test-backend-module-tester-two'), + optional: true, + }); + expect(Task.forCommand).toHaveBeenCalledWith('yarn lint --fix', { + cwd: resolvePath('/root/plugins/test-backend-module-tester-two'), + optional: true, + }); + }); +}); diff --git a/packages/cli/src/lib/new/factories/backendModule.ts b/packages/cli/src/lib/new/factories/backendModule.ts new file mode 100644 index 0000000000..221adcb1f3 --- /dev/null +++ b/packages/cli/src/lib/new/factories/backendModule.ts @@ -0,0 +1,100 @@ +/* + * 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 fs from 'fs-extra'; +import chalk from 'chalk'; +import camelCase from 'lodash/camelCase'; +import { paths } from '../../paths'; +import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; +import { createFactory, CreateContext } from '../types'; +import { addPackageDependency, Task } from '../../tasks'; +import { + moduleIdIdPrompt, + ownerPrompt, + pluginIdPrompt, +} from './common/prompts'; +import { executePluginPackageTemplate } from './common/tasks'; + +type Options = { + id: string; + moduleId: string; + owner?: string; + codeOwnersPath?: string; +}; + +export const backendModule = createFactory({ + name: 'backend-module', + description: 'A new backend module', + optionsDiscovery: async () => ({ + codeOwnersPath: await getCodeownersFilePath(paths.targetRoot), + }), + optionsPrompts: [pluginIdPrompt(), moduleIdIdPrompt(), ownerPrompt()], + async create(options: Options, ctx: CreateContext) { + const { id: pluginId, moduleId } = options; + const dirName = `${pluginId}-backend-module-${moduleId}`; + const name = ctx.scope + ? `@${ctx.scope}/plugin-${dirName}` + : `backstage-plugin-${dirName}`; + + Task.log(); + Task.log(`Creating backend module ${chalk.cyan(name)}`); + + const targetDir = ctx.isMonoRepo + ? paths.resolveTargetRoot('plugins', dirName) + : paths.resolveTargetRoot(`backstage-plugin-${dirName}`); + + const moduleCamelCase = camelCase(moduleId); + const modulePascalCase = + moduleCamelCase[0].toUpperCase() + moduleCamelCase.slice(1); + const moduleVar = `${camelCase(pluginId)}Module${modulePascalCase}`; + await executePluginPackageTemplate(ctx, { + targetDir, + templateName: 'default-backend-module', + values: { + pluginId, + moduleId, + name, + moduleVar, + packageVersion: ctx.defaultVersion, + privatePackage: ctx.private, + npmRegistry: ctx.npmRegistry, + }, + }); + + if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { + await Task.forItem('backend', 'adding dependency', async () => { + await addPackageDependency( + paths.resolveTargetRoot('packages/backend/package.json'), + { + dependencies: { + [name]: `^${ctx.defaultVersion}`, + }, + }, + ); + }); + } + + if (options.owner) { + await addCodeownersEntry(`/plugins/${dirName}`, options.owner); + } + + await Task.forCommand('yarn install', { cwd: targetDir, optional: true }); + await Task.forCommand('yarn lint --fix', { + cwd: targetDir, + optional: true, + }); + }, +}); diff --git a/packages/cli/src/lib/new/factories/common/prompts.ts b/packages/cli/src/lib/new/factories/common/prompts.ts index 9c7672ddfb..428ba8f9d3 100644 --- a/packages/cli/src/lib/new/factories/common/prompts.ts +++ b/packages/cli/src/lib/new/factories/common/prompts.ts @@ -33,6 +33,22 @@ export function pluginIdPrompt(): Prompt<{ id: string }> { }; } +export function moduleIdIdPrompt(): Prompt<{ moduleId: string }> { + return { + type: 'input', + name: 'moduleId', + message: 'Enter the ID of the module [required]', + validate: (value: string) => { + if (!value) { + return 'Please enter the ID of the module'; + } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { + return 'Module IDs must be lowercase and contain only letters, digits, and dashes.'; + } + return true; + }, + }; +} + export function ownerPrompt(): Prompt<{ owner?: string; codeOwnersPath?: string; diff --git a/packages/cli/src/lib/new/factories/index.ts b/packages/cli/src/lib/new/factories/index.ts index 232e762b91..a4bcb2b721 100644 --- a/packages/cli/src/lib/new/factories/index.ts +++ b/packages/cli/src/lib/new/factories/index.ts @@ -16,6 +16,7 @@ export { frontendPlugin } from './frontendPlugin'; export { backendPlugin } from './backendPlugin'; +export { backendModule } from './backendModule'; export { webLibraryPackage } from './webLibraryPackage'; export { pluginCommon } from './pluginCommon'; export { pluginNode } from './pluginNode'; diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index bf89842d5f..96b42fcfb7 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -35,6 +35,8 @@ leaving any imports in place. */ import { version as backendCommon } from '../../../../packages/backend-common/package.json'; +import { version as backendPluginApi } from '../../../../packages/backend-plugin-api/package.json'; +import { version as backendTestUtils } from '../../../../packages/backend-test-utils/package.json'; import { version as cli } from '../../../../packages/cli/package.json'; import { version as config } from '../../../../packages/config/package.json'; import { version as coreAppApi } from '../../../../packages/core-app-api/package.json'; @@ -47,6 +49,8 @@ import { version as scaffolderBackend } from '../../../../plugins/scaffolder-bac export const packageVersions: Record = { '@backstage/backend-common': backendCommon, + '@backstage/backend-plugin-api': backendPluginApi, + '@backstage/backend-test-utils': backendTestUtils, '@backstage/cli': cli, '@backstage/config': config, '@backstage/core-app-api': coreAppApi, diff --git a/packages/cli/templates/default-backend-module/.eslintrc.js b/packages/cli/templates/default-backend-module/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/cli/templates/default-backend-module/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/cli/templates/default-backend-module/README.md.hbs b/packages/cli/templates/default-backend-module/README.md.hbs new file mode 100644 index 0000000000..ad7669beda --- /dev/null +++ b/packages/cli/templates/default-backend-module/README.md.hbs @@ -0,0 +1,5 @@ +# {{name}} + +The {{moduleId}} backend module for the {{pluginId}} plugin. + +_This plugin was created through the Backstage CLI_ diff --git a/packages/cli/templates/default-backend-module/package.json.hbs b/packages/cli/templates/default-backend-module/package.json.hbs new file mode 100644 index 0000000000..07eee73d50 --- /dev/null +++ b/packages/cli/templates/default-backend-module/package.json.hbs @@ -0,0 +1,42 @@ +{ + "name": "{{name}}", + "description": "The {{moduleId}} backend module for the {{pluginId}} plugin.", + "version": "{{packageVersion}}", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", +{{#if privatePackage}} + "private": {{privatePackage}}, +{{/if}} + "publishConfig": { +{{#if npmRegistry}} + "registry": "{{npmRegistry}}", +{{/if}} + "access": "public", + "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", + "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": "{{versionQuery '@backstage/backend-common'}}", + "@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}" + }, + "devDependencies": { + "@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}", + "@backstage/cli": "{{versionQuery '@backstage/cli'}}" + }, + "files": [ + "dist" + ] +} diff --git a/packages/cli/templates/default-backend-module/src/index.ts.hbs b/packages/cli/templates/default-backend-module/src/index.ts.hbs new file mode 100644 index 0000000000..0cf1f00d42 --- /dev/null +++ b/packages/cli/templates/default-backend-module/src/index.ts.hbs @@ -0,0 +1,8 @@ +/***/ +/** + * The {{moduleId}} backend module for the {{pluginId}} plugin. + * + * @packageDocumentation + */ + +export { {{moduleVar}} } from './module'; diff --git a/packages/cli/templates/default-backend-module/src/module.ts.hbs b/packages/cli/templates/default-backend-module/src/module.ts.hbs new file mode 100644 index 0000000000..8c895a49c0 --- /dev/null +++ b/packages/cli/templates/default-backend-module/src/module.ts.hbs @@ -0,0 +1,14 @@ +import { coreServices, createBackendModule } from '@backstage/backend-plugin-api'; + +export const {{moduleVar}} = createBackendModule({ + pluginId: '{{pluginId}}', + moduleId: '{{moduleId}}', + register(reg) { + reg.registerInit({ + deps: { logger: coreServices.logger }, + async init({ logger }) { + logger.info('Hello World!') + }, + }); + }, +}); diff --git a/packages/cli/templates/default-backend-module/tsconfig.json b/packages/cli/templates/default-backend-module/tsconfig.json new file mode 100644 index 0000000000..5ae9aeb62d --- /dev/null +++ b/packages/cli/templates/default-backend-module/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@backstage/cli/config/tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"], + "compilerOptions": { + "outDir": "dist-types", + "rootDir": "." + } +} diff --git a/packages/config-loader/src/sources/ObservableConfigProxy.test.ts b/packages/config-loader/src/sources/ObservableConfigProxy.test.ts index d1fccf3e80..f072536489 100644 --- a/packages/config-loader/src/sources/ObservableConfigProxy.test.ts +++ b/packages/config-loader/src/sources/ObservableConfigProxy.test.ts @@ -25,21 +25,17 @@ describe('ObservableConfigProxy', () => { const sub = config.subscribe(fn); expect(config.getOptionalNumber('x')).toBe(undefined); - config.setConfig(new ConfigReader({})); - expect(fn).toHaveBeenCalledTimes(1); - expect(config.getOptionalNumber('x')).toBe(undefined); - config.setConfig(new ConfigReader({ x: 1 })); - expect(fn).toHaveBeenCalledTimes(2); + expect(fn).toHaveBeenCalledTimes(1); expect(config.getOptionalNumber('x')).toBe(1); config.setConfig(new ConfigReader({ x: 3 })); - expect(fn).toHaveBeenCalledTimes(3); + expect(fn).toHaveBeenCalledTimes(2); sub.unsubscribe(); expect(config.getOptionalNumber('x')).toBe(3); config.setConfig(new ConfigReader({ x: 5 })); - expect(fn).toHaveBeenCalledTimes(3); + expect(fn).toHaveBeenCalledTimes(2); expect(config.getOptionalNumber('x')).toBe(5); }); @@ -58,18 +54,10 @@ describe('ObservableConfigProxy', () => { expect(config2.getOptionalNumber('x')).toBe(undefined); expect(config3.getOptionalNumber('x')).toBe(undefined); - config1.setConfig(new ConfigReader({})); + config1.setConfig(new ConfigReader({ x: 1, a: { x: 2, b: { x: 3 } } })); expect(fn1).toHaveBeenCalledTimes(1); expect(fn2).toHaveBeenCalledTimes(1); expect(fn3).toHaveBeenCalledTimes(1); - expect(config1.getOptionalNumber('x')).toBe(undefined); - expect(config2.getOptionalNumber('x')).toBe(undefined); - expect(config3.getOptionalNumber('x')).toBe(undefined); - - config1.setConfig(new ConfigReader({ x: 1, a: { x: 2, b: { x: 3 } } })); - expect(fn1).toHaveBeenCalledTimes(2); - expect(fn2).toHaveBeenCalledTimes(2); - expect(fn3).toHaveBeenCalledTimes(2); expect(config1.getNumber('x')).toBe(1); expect(config2.getNumber('x')).toBe(2); expect(config3.getNumber('x')).toBe(3); @@ -79,9 +67,9 @@ describe('ObservableConfigProxy', () => { sub3.unsubscribe(); config1.setConfig(new ConfigReader({ x: 4, a: { x: 5, b: { x: 6 } } })); - expect(fn1).toHaveBeenCalledTimes(2); - expect(fn2).toHaveBeenCalledTimes(2); - expect(fn3).toHaveBeenCalledTimes(2); + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn2).toHaveBeenCalledTimes(1); + expect(fn3).toHaveBeenCalledTimes(1); expect(config1.getNumber('x')).toBe(4); expect(config2.getNumber('x')).toBe(5); expect(config3.getNumber('x')).toBe(6); @@ -140,4 +128,43 @@ describe('ObservableConfigProxy', () => { 'Only the root config can be closed', ); }); + + it('should only notify subscribers when the config data actually changes (ignoring key order)', () => { + const config = ObservableConfigProxy.create(new AbortController()); + const fn = jest.fn(); + config.subscribe(fn); + + expect(config.getOptionalNumber('a')).toBe(undefined); + + config.setConfig(new ConfigReader({})); + expect(fn).toHaveBeenCalledTimes(0); + expect(config.getOptionalNumber('a')).toBe(undefined); + + config.setConfig( + new ConfigReader({ + a: 1, + b: 1, + }), + ); + expect(fn).toHaveBeenCalledTimes(1); + expect(config.getOptionalNumber('a')).toBe(1); + + config.setConfig( + new ConfigReader({ + b: 1, + a: 1, + }), + ); + expect(fn).toHaveBeenCalledTimes(1); + expect(config.getOptionalNumber('a')).toBe(1); + + config.setConfig( + new ConfigReader({ + b: 1, + a: 2, + }), + ); + expect(fn).toHaveBeenCalledTimes(2); + expect(config.getOptionalNumber('a')).toBe(2); + }); }); diff --git a/packages/config-loader/src/sources/ObservableConfigProxy.ts b/packages/config-loader/src/sources/ObservableConfigProxy.ts index 59be836160..3d2084beac 100644 --- a/packages/config-loader/src/sources/ObservableConfigProxy.ts +++ b/packages/config-loader/src/sources/ObservableConfigProxy.ts @@ -16,6 +16,7 @@ import { Config, ConfigReader } from '@backstage/config'; import { JsonValue } from '@backstage/types'; +import isEqual from 'lodash/isEqual'; export class ObservableConfigProxy implements Config { private config: Config = new ConfigReader({}); @@ -40,12 +41,22 @@ export class ObservableConfigProxy implements Config { if (this.parent) { throw new Error('immutable'); } + + // We only notify subscribers if the data contents of the config actually + // changed. If they didn't, there's no point in callers trying to re-read + // them. However we still want to replace the local config object, since its + // runtime implementation could be entirely different. + const changed = !isEqual(this.config.get(), config.get()); + this.config = config; - for (const subscriber of this.subscribers) { - try { - subscriber(); - } catch (error) { - console.error(`Config subscriber threw error, ${error}`); + + if (changed) { + for (const subscriber of this.subscribers) { + try { + subscriber(); + } catch (error) { + console.error(`Config subscriber threw error, ${error}`); + } } } } diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 5746598ffd..8b344e748c 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -56,6 +56,7 @@ import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import PropTypes from 'prop-types'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; @@ -113,7 +114,7 @@ export type ApiFactoryScope = 'default' | 'app' | 'static'; // @public export const ApiProvider: { - (props: PropsWithChildren): JSX.Element; + (props: PropsWithChildren): React_2.JSX.Element; propTypes: { apis: PropTypes.Validator< NonNullable< @@ -235,7 +236,7 @@ export type AppRouteBinder = < ) => void; // @public -export function AppRouter(props: AppRouterProps): JSX.Element; +export function AppRouter(props: AppRouterProps): React_2.JSX.Element; // @public export interface AppRouterProps { @@ -368,7 +369,9 @@ export type ErrorBoundaryFallbackProps = PropsWithChildren<{ }>; // @public -export const FeatureFlagged: (props: FeatureFlaggedProps) => JSX.Element; +export const FeatureFlagged: ( + props: FeatureFlaggedProps, +) => React_2.JSX.Element; // @public export type FeatureFlaggedProps = { @@ -535,10 +538,10 @@ export type OAuth2Session = { idToken: string; accessToken: string; scopes: Set; - expiresAt: Date; + expiresAt?: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; + backstageIdentity?: BackstageIdentityResponse; }; // @public diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts index 6f941379a7..8ca818ebd8 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts @@ -76,6 +76,37 @@ describe('OAuth2', () => { ); }); + it('should forward backstage identity', async () => { + getSession = jest.fn().mockResolvedValue({ + providerInfo: { accessToken: 'access-token', expiresAt: theFuture }, + backstageIdentity: { + token: 'a.b.c', + expiresAt: theFuture, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, + }, + }); + const oauth2 = OAuth2.create({ + configApi: configApi, + scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`), + oauthRequestApi: new MockOAuthApi(), + discoveryApi: UrlPatternDiscovery.compile('http://example.com'), + }); + + await expect(oauth2.getBackstageIdentity()).resolves.toEqual({ + token: 'a.b.c', + expiresAt: theFuture, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, + }); + }); + it('should get refreshed id token', async () => { getSession = jest.fn().mockResolvedValue({ providerInfo: { idToken: 'id-token', expiresAt: theFuture }, diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts index fd40a9c8fb..6be7cfc89d 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts @@ -30,6 +30,7 @@ import { SessionState, SessionApi, BackstageIdentityApi, + BackstageUserIdentity, } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { OAuth2Session } from './types'; @@ -49,10 +50,14 @@ export type OAuth2Response = { accessToken: string; idToken: string; scope: string; - expiresInSeconds: number; + expiresInSeconds?: number; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; + backstageIdentity: { + token: string; + expiresInSeconds?: number; + identity: BackstageUserIdentity; + }; }; const DEFAULT_PROVIDER = { @@ -92,8 +97,11 @@ export default class OAuth2 environment, provider, oauthRequestApi: oauthRequestApi, - sessionTransform(res: OAuth2Response): OAuth2Session { - return { + sessionTransform({ + backstageIdentity, + ...res + }: OAuth2Response): OAuth2Session { + const session: OAuth2Session = { ...res, providerInfo: { idToken: res.providerInfo.idToken, @@ -102,11 +110,26 @@ export default class OAuth2 scopeTransform, res.providerInfo.scope, ), - expiresAt: new Date( - Date.now() + res.providerInfo.expiresInSeconds * 1000, - ), + expiresAt: res.providerInfo.expiresInSeconds + ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000) + : undefined, }, }; + if (backstageIdentity) { + // TODO(Rugvip): This fallback can be removed a few releases after 1.18. It's there to avoid + // breaking deployments that update their frontend before updating their backend. + const expInSec = + backstageIdentity.expiresInSeconds ?? + res.providerInfo.expiresInSeconds; + session.backstageIdentity = { + token: backstageIdentity.token, + identity: backstageIdentity.identity, + expiresAt: expInSec + ? new Date(Date.now() + expInSec * 1000) + : undefined, + }; + } + return session; }, popupOptions, }); @@ -116,9 +139,21 @@ export default class OAuth2 defaultScopes: new Set(defaultScopes), sessionScopes: (session: OAuth2Session) => session.providerInfo.scopes, sessionShouldRefresh: (session: OAuth2Session) => { - const expiresInSec = - (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000; - return expiresInSec < 60 * 5; + // TODO(Rugvip): Optimize to use separate checks for provider vs backstage session expiration + let min = Infinity; + if (session.providerInfo?.expiresAt) { + min = Math.min( + min, + (session.providerInfo.expiresAt.getTime() - Date.now()) / 1000, + ); + } + if (session.backstageIdentity?.expiresAt) { + min = Math.min( + min, + (session.backstageIdentity.expiresAt.getTime() - Date.now()) / 1000, + ); + } + return min < 60 * 5; }, }); diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts index bd3bb95ba6..e51de63dbe 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/types.ts @@ -31,8 +31,8 @@ export type OAuth2Session = { idToken: string; accessToken: string; scopes: Set; - expiresAt: Date; + expiresAt?: Date; }; profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; + backstageIdentity?: BackstageIdentityResponse; }; diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index df34abdd03..54c26039a3 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -54,7 +54,9 @@ import { TooltipProps } from '@material-ui/core/Tooltip'; import { WithStyles } from '@material-ui/core/styles'; // @public -export function AlertDisplay(props: AlertDisplayProps): JSX.Element | null; +export function AlertDisplay( + props: AlertDisplayProps, +): React_2.JSX.Element | null; // @public export type AlertDisplayProps = { @@ -66,7 +68,7 @@ export type AlertDisplayProps = { }; // @public -export function Avatar(props: AvatarProps): JSX.Element; +export function Avatar(props: AvatarProps): React_2.JSX.Element; // @public (undocumented) export type AvatarClassKey = 'avatar'; @@ -94,7 +96,7 @@ export type BackstageOverrides = Overrides & { export type BoldHeaderClassKey = 'root' | 'title' | 'subheader'; // @public -export function BottomLink(props: BottomLinkProps): JSX.Element; +export function BottomLink(props: BottomLinkProps): React_2.JSX.Element; // @public (undocumented) export type BottomLinkClassKey = 'root' | 'boxTitle' | 'arrow'; @@ -109,7 +111,7 @@ export type BottomLinkProps = { // Warning: (ae-forgotten-export) The symbol "Props_19" needs to be exported by the entry point index.d.ts // // @public -export function Breadcrumbs(props: Props_19): JSX.Element; +export function Breadcrumbs(props: Props_19): React_2.JSX.Element; // @public (undocumented) export type BreadcrumbsClickableTextClassKey = 'root'; @@ -120,7 +122,7 @@ export type BreadcrumbsStyledBoxClassKey = 'root'; // Warning: (ae-forgotten-export) The symbol "IconComponentProps" needs to be exported by the entry point index.d.ts // // @public -export function BrokenImageIcon(props: IconComponentProps): JSX.Element; +export function BrokenImageIcon(props: IconComponentProps): React_2.JSX.Element; // @public @deprecated (undocumented) export const Button: (props: LinkButtonProps) => JSX.Element; @@ -134,22 +136,24 @@ export type CardActionsTopRightClassKey = 'root'; // Warning: (ae-forgotten-export) The symbol "CardTabProps" needs to be exported by the entry point index.d.ts // // @public -export function CardTab(props: PropsWithChildren): JSX.Element; +export function CardTab( + props: PropsWithChildren, +): React_2.JSX.Element; // @public (undocumented) export type CardTabClassKey = 'root' | 'selected'; // @public (undocumented) -export function CatalogIcon(props: IconComponentProps): JSX.Element; +export function CatalogIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) -export function ChatIcon(props: IconComponentProps): JSX.Element; +export function ChatIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) export type ClosedDropdownClassKey = 'icon'; // @public -export function CodeSnippet(props: CodeSnippetProps): JSX.Element; +export function CodeSnippet(props: CodeSnippetProps): React_2.JSX.Element; // @public export interface CodeSnippetProps { @@ -164,14 +168,16 @@ export interface CodeSnippetProps { // Warning: (ae-forgotten-export) The symbol "Props_13" needs to be exported by the entry point index.d.ts // // @public -export function Content(props: PropsWithChildren): JSX.Element; +export function Content( + props: PropsWithChildren, +): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ContentHeaderProps" needs to be exported by the entry point index.d.ts // // @public export function ContentHeader( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type ContentHeaderClassKey = @@ -182,7 +188,7 @@ export type ContentHeaderClassKey = | 'title'; // @public -export function CopyTextButton(props: CopyTextButtonProps): JSX.Element; +export function CopyTextButton(props: CopyTextButtonProps): React_2.JSX.Element; // @public export interface CopyTextButtonProps { @@ -193,7 +199,9 @@ export interface CopyTextButtonProps { } // @public -export function CreateButton(props: CreateButtonProps): JSX.Element | null; +export function CreateButton( + props: CreateButtonProps, +): React_2.JSX.Element | null; // @public export type CreateButtonProps = { @@ -204,12 +212,12 @@ export type CreateButtonProps = { export type CustomProviderClassKey = 'form' | 'button'; // @public (undocumented) -export function DashboardIcon(props: IconComponentProps): JSX.Element; +export function DashboardIcon(props: IconComponentProps): React_2.JSX.Element; // @public export function DependencyGraph( props: DependencyGraphProps, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type DependencyGraphDefaultLabelClassKey = 'text'; @@ -301,7 +309,7 @@ export namespace DependencyGraphTypes { // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const DismissableBanner: (props: Props) => JSX.Element; +export const DismissableBanner: (props: Props) => React_2.JSX.Element; // @public (undocumented) export type DismissableBannerClassKey = @@ -317,15 +325,15 @@ export type DismissableBannerClassKey = export type DismissbleBannerClassKey = DismissableBannerClassKey; // @public (undocumented) -export function DocsIcon(props: IconComponentProps): JSX.Element; +export function DocsIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) -export function EmailIcon(props: IconComponentProps): JSX.Element; +export function EmailIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_2" needs to be exported by the entry point index.d.ts // // @public -export function EmptyState(props: Props_2): JSX.Element; +export function EmptyState(props: Props_2): React_2.JSX.Element; // @public (undocumented) export type EmptyStateClassKey = 'root' | 'action' | 'imageContainer'; @@ -347,7 +355,7 @@ export type ErrorBoundaryProps = { // Warning: (ae-forgotten-export) The symbol "IErrorPageProps" needs to be exported by the entry point index.d.ts // // @public -export function ErrorPage(props: IErrorPageProps): JSX.Element; +export function ErrorPage(props: IErrorPageProps): React_2.JSX.Element; // @public (undocumented) export type ErrorPageClassKey = 'container' | 'title' | 'subtitle'; @@ -355,7 +363,7 @@ export type ErrorPageClassKey = 'container' | 'title' | 'subtitle'; // @public export function ErrorPanel( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type ErrorPanelClassKey = 'text' | 'divider'; @@ -382,18 +390,18 @@ export type FeatureCalloutCircleClassKey = // @public export function FeatureCalloutCircular( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type FiltersContainerClassKey = 'root' | 'title'; // @public -export function Gauge(props: GaugeProps): JSX.Element; +export function Gauge(props: GaugeProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_10" needs to be exported by the entry point index.d.ts // // @public -export function GaugeCard(props: Props_10): JSX.Element; +export function GaugeCard(props: Props_10): React_2.JSX.Element; // @public (undocumented) export type GaugeCardClassKey = 'root'; @@ -429,18 +437,20 @@ export type GaugePropsGetColorOptions = { }; // @public (undocumented) -export function GitHubIcon(props: IconComponentProps): JSX.Element; +export function GitHubIcon(props: IconComponentProps): React_2.JSX.Element; // @public (undocumented) -export function GroupIcon(props: IconComponentProps): JSX.Element; +export function GroupIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_14" needs to be exported by the entry point index.d.ts // // @public -export function Header(props: PropsWithChildren): JSX.Element; +export function Header(props: PropsWithChildren): React_2.JSX.Element; // @public (undocumented) -export function HeaderActionMenu(props: HeaderActionMenuProps): JSX.Element; +export function HeaderActionMenu( + props: HeaderActionMenuProps, +): React_2.JSX.Element; // @public (undocumented) export type HeaderActionMenuItem = { @@ -471,7 +481,7 @@ export type HeaderClassKey = // Warning: (ae-forgotten-export) The symbol "Props_5" needs to be exported by the entry point index.d.ts // // @public -export function HeaderIconLinkRow(props: Props_5): JSX.Element; +export function HeaderIconLinkRow(props: Props_5): React_2.JSX.Element; // @public (undocumented) export type HeaderIconLinkRowClassKey = 'links'; @@ -479,7 +489,7 @@ export type HeaderIconLinkRowClassKey = 'links'; // Warning: (ae-forgotten-export) The symbol "HeaderLabelProps" needs to be exported by the entry point index.d.ts // // @public -export function HeaderLabel(props: HeaderLabelProps): JSX.Element; +export function HeaderLabel(props: HeaderLabelProps): React_2.JSX.Element; // @public (undocumented) export type HeaderLabelClassKey = 'root' | 'label' | 'value'; @@ -487,7 +497,7 @@ export type HeaderLabelClassKey = 'root' | 'label' | 'value'; // Warning: (ae-forgotten-export) The symbol "HeaderTabsProps" needs to be exported by the entry point index.d.ts // // @public -export function HeaderTabs(props: HeaderTabsProps): JSX.Element; +export function HeaderTabs(props: HeaderTabsProps): React_2.JSX.Element; // @public (undocumented) export type HeaderTabsClassKey = @@ -497,14 +507,14 @@ export type HeaderTabsClassKey = | 'tabRoot'; // @public (undocumented) -export function HelpIcon(props: IconComponentProps): JSX.Element; +export function HelpIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_6" needs to be exported by the entry point index.d.ts // // @public export function HorizontalScrollGrid( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public (undocumented) export type HorizontalScrollGridClassKey = @@ -563,16 +573,16 @@ export type InfoCardVariants = 'flex' | 'fullHeight' | 'gridItem'; // Warning: (ae-forgotten-export) The symbol "IntroCardProps" needs to be exported by the entry point index.d.ts // // @public -export function IntroCard(props: IntroCardProps): JSX.Element; +export function IntroCard(props: IntroCardProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ItemCardProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ItemCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated -export function ItemCard(props: ItemCardProps): JSX.Element; +export function ItemCard(props: ItemCardProps): React_2.JSX.Element; // @public -export function ItemCardGrid(props: ItemCardGridProps): JSX.Element; +export function ItemCardGrid(props: ItemCardGridProps): React_2.JSX.Element; // @public (undocumented) export type ItemCardGridClassKey = 'root'; @@ -585,7 +595,7 @@ export type ItemCardGridProps = Partial> & { }; // @public -export function ItemCardHeader(props: ItemCardHeaderProps): JSX.Element; +export function ItemCardHeader(props: ItemCardHeaderProps): React_2.JSX.Element; // @public (undocumented) export type ItemCardHeaderClassKey = 'root'; @@ -603,7 +613,7 @@ export type ItemCardHeaderProps = Partial> & { // Warning: (ae-missing-release-tag) "Lifecycle" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Lifecycle(props: Props_7): JSX.Element; +export function Lifecycle(props: Props_7): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "LifecycleClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -614,7 +624,7 @@ export type LifecycleClassKey = 'alpha' | 'beta'; // Warning: (ae-missing-release-tag) "LinearGauge" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function LinearGauge(props: Props_11): JSX.Element | null; +export function LinearGauge(props: Props_11): React_2.JSX.Element | null; // Warning: (ae-missing-release-tag) "Link" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -644,7 +654,7 @@ export type LinkProps = Omit & export type LoginRequestListItemClassKey = 'root'; // @public -export function LogViewer(props: LogViewerProps): JSX.Element; +export function LogViewer(props: LogViewerProps): React_2.JSX.Element; // @public export type LogViewerClassKey = @@ -691,7 +701,7 @@ export interface LogViewerProps { // Warning: (ae-missing-release-tag) "MarkdownContent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function MarkdownContent(props: Props_8): JSX.Element; +export function MarkdownContent(props: Props_8): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "MarkdownContentClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -727,7 +737,9 @@ export type MicDropClassKey = 'micDrop'; // Warning: (ae-missing-release-tag) "MissingAnnotationEmptyState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function MissingAnnotationEmptyState(props: Props_3): JSX.Element; +export function MissingAnnotationEmptyState( + props: Props_3, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "MissingAnnotationEmptyStateClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -735,7 +747,9 @@ export function MissingAnnotationEmptyState(props: Props_3): JSX.Element; export type MissingAnnotationEmptyStateClassKey = 'code'; // @public -export const MobileSidebar: (props: MobileSidebarProps) => JSX.Element | null; +export const MobileSidebar: ( + props: MobileSidebarProps, +) => React_2.JSX.Element | null; // @public export type MobileSidebarProps = { @@ -745,7 +759,7 @@ export type MobileSidebarProps = { // Warning: (ae-missing-release-tag) "OAuthRequestDialog" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function OAuthRequestDialog(_props: {}): JSX.Element; +export function OAuthRequestDialog(_props: {}): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "OAuthRequestDialogClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -765,7 +779,7 @@ export type OpenedDropdownClassKey = 'icon'; // Warning: (ae-missing-release-tag) "OverflowTooltip" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function OverflowTooltip(props: Props_9): JSX.Element; +export function OverflowTooltip(props: Props_9): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "OverflowTooltipClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -776,7 +790,7 @@ export type OverflowTooltipClassKey = 'container'; // Warning: (ae-missing-release-tag) "Page" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Page(props: Props_16): JSX.Element; +export function Page(props: Props_16): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "PageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -789,19 +803,19 @@ export type PageClassKey = 'root'; // @public (undocumented) export function PageWithHeader( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "Progress" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function Progress( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // @public export const ProxiedSignInPage: ( props: ProxiedSignInPageProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public export type ProxiedSignInPageProps = SignInPageProps & { @@ -812,7 +826,7 @@ export type ProxiedSignInPageProps = SignInPageProps & { // Warning: (ae-missing-release-tag) "ResponseErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function ResponseErrorPanel(props: ErrorPanelProps): JSX.Element; +export function ResponseErrorPanel(props: ErrorPanelProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "ResponseErrorPanelClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -822,12 +836,14 @@ export type ResponseErrorPanelClassKey = 'text' | 'divider'; // Warning: (ae-missing-release-tag) "RoutedTabs" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function RoutedTabs(props: { routes: SubRoute_2[] }): JSX.Element; +export function RoutedTabs(props: { + routes: SubRoute_2[]; +}): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "SelectProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function Select(props: SelectProps): JSX.Element; +export function Select(props: SelectProps): React_2.JSX.Element; // @public (undocumented) export type SelectClassKey = @@ -851,7 +867,7 @@ export type SelectItem = { }; // @public -export const Sidebar: (props: SidebarProps) => JSX.Element; +export const Sidebar: (props: SidebarProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SIDEBAR_INTRO_LOCAL_STORAGE" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -905,10 +921,10 @@ export const SidebarDivider: React_2.ComponentType< export type SidebarDividerClassKey = 'root'; // @public -export const SidebarExpandButton: () => JSX.Element | null; +export const SidebarExpandButton: () => React_2.JSX.Element | null; // @public -export const SidebarGroup: (props: SidebarGroupProps) => JSX.Element; +export const SidebarGroup: (props: SidebarGroupProps) => React_2.JSX.Element; // @public export interface SidebarGroupProps extends BottomNavigationActionProps { @@ -920,7 +936,7 @@ export interface SidebarGroupProps extends BottomNavigationActionProps { // Warning: (ae-missing-release-tag) "SidebarIntro" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SidebarIntro(_props: {}): JSX.Element | null; +export function SidebarIntro(_props: {}): React_2.JSX.Element | null; // @public (undocumented) export type SidebarIntroClassKey = @@ -967,7 +983,7 @@ export type SidebarOpenState = { export function SidebarOpenStateProvider(props: { children: ReactNode; value: SidebarOpenState; -}): JSX.Element; +}): React_2.JSX.Element; // @public (undocumented) export type SidebarOptions = { @@ -978,7 +994,7 @@ export type SidebarOptions = { // Warning: (ae-missing-release-tag) "SidebarPage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SidebarPage(props: SidebarPageProps): JSX.Element; +export function SidebarPage(props: SidebarPageProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SidebarPageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1011,7 +1027,7 @@ export type SidebarPinStateContextType = { export function SidebarPinStateProvider(props: { children: ReactNode; value: SidebarPinStateContextType; -}): JSX.Element; +}): React_2.JSX.Element; // @public (undocumented) export type SidebarProps = { @@ -1036,7 +1052,9 @@ export const SidebarScrollWrapper: React_2.ComponentType< // Warning: (ae-missing-release-tag) "SidebarSearchField" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SidebarSearchField(props: SidebarSearchFieldProps): JSX.Element; +export function SidebarSearchField( + props: SidebarSearchFieldProps, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SidebarSpace" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1067,12 +1085,14 @@ export const SidebarSpacer: React_2.ComponentType< export type SidebarSpacerClassKey = 'root'; // @public -export const SidebarSubmenu: (props: SidebarSubmenuProps) => JSX.Element; +export const SidebarSubmenu: ( + props: SidebarSubmenuProps, +) => React_2.JSX.Element; // @public export const SidebarSubmenuItem: ( props: SidebarSubmenuItemProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type SidebarSubmenuItemDropdownItem = { @@ -1100,7 +1120,7 @@ export type SidebarSubmenuProps = { // Warning: (ae-missing-release-tag) "SignInPage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SignInPage(props: Props_17): JSX.Element; +export function SignInPage(props: Props_17): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SignInPageClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1123,7 +1143,7 @@ export type SignInProviderConfig = { // @public (undocumented) export function SimpleStepper( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SimpleStepperFooterClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1136,7 +1156,7 @@ export type SimpleStepperFooterClassKey = 'root'; // @public (undocumented) export function SimpleStepperStep( props: PropsWithChildren, -): JSX.Element; +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SimpleStepperStepClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1146,7 +1166,9 @@ export type SimpleStepperStepClassKey = 'end'; // Warning: (ae-missing-release-tag) "StatusAborted" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusAborted(props: PropsWithChildren<{}>): JSX.Element; +export function StatusAborted( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1163,33 +1185,39 @@ export type StatusClassKey = // Warning: (ae-missing-release-tag) "StatusError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusError(props: PropsWithChildren<{}>): JSX.Element; +export function StatusError(props: PropsWithChildren<{}>): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusOK" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusOK(props: PropsWithChildren<{}>): JSX.Element; +export function StatusOK(props: PropsWithChildren<{}>): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusPending" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusPending(props: PropsWithChildren<{}>): JSX.Element; +export function StatusPending( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusRunning" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusRunning(props: PropsWithChildren<{}>): JSX.Element; +export function StatusRunning( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StatusWarning" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StatusWarning(props: PropsWithChildren<{}>): JSX.Element; +export function StatusWarning( + props: PropsWithChildren<{}>, +): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "Props_12" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "StructuredMetadataTable" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function StructuredMetadataTable(props: Props_12): JSX.Element; +export function StructuredMetadataTable(props: Props_12): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "StructuredMetadataTableListClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1211,7 +1239,7 @@ export type SubmenuOptions = { // Warning: (ae-missing-release-tag) "SubvalueCell" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SubvalueCell(props: SubvalueCellProps): JSX.Element; +export function SubvalueCell(props: SubvalueCellProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SubvalueCellClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1222,7 +1250,7 @@ export type SubvalueCellClassKey = 'value' | 'subvalue'; // Warning: (ae-missing-release-tag) "SupportButton" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function SupportButton(props: SupportButtonProps): JSX.Element; +export function SupportButton(props: SupportButtonProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SupportButtonClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1272,7 +1300,9 @@ export type Tab = { // Warning: (ae-missing-release-tag) "TabbedCard" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function TabbedCard(props: PropsWithChildren): JSX.Element; +export function TabbedCard( + props: PropsWithChildren, +): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "TabbedCardClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1283,7 +1313,7 @@ export type TabbedCardClassKey = 'root' | 'indicator'; // Warning: (ae-missing-release-tag) "TabbedLayout" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function TabbedLayout(props: PropsWithChildren<{}>): JSX.Element; +export function TabbedLayout(props: PropsWithChildren<{}>): React_2.JSX.Element; // @public (undocumented) export namespace TabbedLayout { @@ -1296,7 +1326,9 @@ export namespace TabbedLayout { // Warning: (ae-missing-release-tag) "Table" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function Table(props: TableProps): JSX.Element; +export function Table( + props: TableProps, +): React_2.JSX.Element; // @public (undocumented) export namespace Table { @@ -1356,6 +1388,8 @@ export interface TableProps // (undocumented) initialState?: TableState; // (undocumented) + isLoading?: boolean; + // (undocumented) onStateChange?: (state: TableState) => any; // (undocumented) subtitle?: string; @@ -1383,7 +1417,7 @@ export function TrendLine( Pick & { title?: string; }, -): JSX.Element | null; +): React_2.JSX.Element | null; // @public export function useContent(): { @@ -1401,7 +1435,7 @@ export function useQueryParamState( ): [T | undefined, SetQueryParams]; // @public (undocumented) -export function UserIcon(props: IconComponentProps): JSX.Element; +export function UserIcon(props: IconComponentProps): React_2.JSX.Element; // @public export class UserIdentity implements IdentityApi { @@ -1447,13 +1481,13 @@ export const useSidebarPinState: () => SidebarPinState; export function useSupportConfig(): SupportConfig; // @public (undocumented) -export function WarningIcon(props: IconComponentProps): JSX.Element; +export function WarningIcon(props: IconComponentProps): React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "WarningProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "WarningPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function WarningPanel(props: WarningProps): JSX.Element; +export function WarningPanel(props: WarningProps): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "WarningPanelClassKey" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/core-components/src/components/Table/Table.stories.tsx b/packages/core-components/src/components/Table/Table.stories.tsx index 807d58057d..b38941012c 100644 --- a/packages/core-components/src/components/Table/Table.stories.tsx +++ b/packages/core-components/src/components/Table/Table.stories.tsx @@ -89,6 +89,43 @@ export const DefaultTable = () => { ); }; +export const LoadingTable = () => { + const classes = useStyles(); + const columns: TableColumn[] = [ + { + title: 'Column 1', + field: 'col1', + highlight: true, + }, + { + title: 'Column 2', + field: 'col2', + }, + { + title: 'Numeric value', + field: 'number', + type: 'numeric', + }, + { + title: 'A Date', + field: 'date', + type: 'date', + }, + ]; + + return ( +
+ + + ); +}; + export const EmptyTable = () => { const classes = useStyles(); const columns: TableColumn[] = [ diff --git a/packages/core-components/src/components/Table/Table.test.tsx b/packages/core-components/src/components/Table/Table.test.tsx index ffdca26d98..43c8cfe41d 100644 --- a/packages/core-components/src/components/Table/Table.test.tsx +++ b/packages/core-components/src/components/Table/Table.test.tsx @@ -48,6 +48,11 @@ describe('
', () => { expect(rendered.getByText('second value, second row')).toBeInTheDocument(); }); + it('renders loading without exploding', async () => { + const rendered = await renderInTestApp(
); + expect(rendered.getByTestId('loading-indicator')).toBeInTheDocument(); + }); + describe('with style rows', () => { describe('with CSS Properties object', () => { const styledColumn2 = { diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx index 5cea3dd6b6..9f2b04c214 100644 --- a/packages/core-components/src/components/Table/Table.tsx +++ b/packages/core-components/src/components/Table/Table.tsx @@ -53,6 +53,7 @@ import React, { import { SelectProps } from '../Select/Select'; import { Filter, Filters, SelectedFilters, Without } from './Filters'; +import CircularProgress from '@material-ui/core/CircularProgress'; // Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51 const tableIcons: Icons = { @@ -236,6 +237,7 @@ export interface TableProps filters?: TableFilter[]; initialState?: TableState; emptyContent?: ReactNode; + isLoading?: boolean; onStateChange?: (state: TableState) => any; } @@ -309,6 +311,7 @@ export function Table(props: TableProps) { emptyContent, onStateChange, components, + isLoading: isLoading, ...restProps } = props; const tableClasses = useTableStyles(); @@ -470,6 +473,28 @@ export function Table(props: TableProps) { const columnCount = columns.length; const Body = useCallback( bodyProps => { + if (isLoading) { + return ( + + + + + + ); + } + if (emptyContent && hasNoRows) { return ( @@ -482,7 +507,7 @@ export function Table(props: TableProps) { return ; }, - [hasNoRows, emptyContent, columnCount], + [hasNoRows, emptyContent, columnCount, isLoading], ); return ( diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 5577d94359..930f1f979d 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -45,7 +45,7 @@ export const analyticsApiRef: ApiRef; export const AnalyticsContext: (options: { attributes: Partial; children: ReactNode; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export type AnalyticsContextValue = CommonAnalyticsContext & { @@ -214,6 +214,7 @@ export type BackstageIdentityApi = { // @public export type BackstageIdentityResponse = { token: string; + expiresAt?: Date; identity: BackstageUserIdentity; }; @@ -782,7 +783,9 @@ export function withApis( ): ( WrappedComponent: React_2.ComponentType, ) => { - (props: React_2.PropsWithChildren>): JSX.Element; + ( + props: React_2.PropsWithChildren>, + ): React_2.JSX.Element; displayName: string; }; ``` diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 62d0d5c810..d9aec2ca5a 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -226,6 +226,11 @@ export type BackstageIdentityResponse = { */ token: string; + /** + * The time at which the token expires. If not set, it can be assumed that the token does not expire. + */ + expiresAt?: Date; + /** * Identity information derived from the token. */ diff --git a/packages/integration-react/api-report.md b/packages/integration-react/api-report.md index 3af0704990..c1a2e34907 100644 --- a/packages/integration-react/api-report.md +++ b/packages/integration-react/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { ApiFactory } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; @@ -13,6 +11,7 @@ import { Config } from '@backstage/config'; import { OAuthApi } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { SessionApi } from '@backstage/core-plugin-api'; @@ -108,7 +107,7 @@ export interface ScmAuthTokenResponse { // @public export const ScmIntegrationIcon: ( props: ScmIntegrationIconProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type ScmIntegrationIconProps = { diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index ec68a20f36..b8652bc98f 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -53,7 +54,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index 7266950ac4..f64c06257e 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -39,16 +39,58 @@ export type AwsS3IntegrationConfig = { }; // @public -export type AzureClientSecretCredential = { +export type AzureClientSecretCredential = AzureCredentialBase & { + kind: 'ClientSecret'; tenantId: string; clientId: string; clientSecret: string; }; // @public -export type AzureCredential = +export type AzureCredentialBase = { + kind: AzureDevOpsCredentialKind; + organizations?: string[]; +}; + +// @public +export type AzureDevOpsCredential = | AzureClientSecretCredential - | AzureManagedIdentityCredential; + | AzureManagedIdentityCredential + | PersonalAccessTokenCredential; + +// @public +export type AzureDevOpsCredentialKind = + | 'PersonalAccessToken' + | 'ClientSecret' + | 'ManagedIdentity'; + +// @public +export type AzureDevOpsCredentialLike = Omit< + Partial & + Partial & + Partial, + 'kind' +>; + +// @public +export type AzureDevOpsCredentials = { + headers: { + [name: string]: string; + }; + token: string; + type: AzureDevOpsCredentialType; +}; + +// @public +export interface AzureDevOpsCredentialsProvider { + // (undocumented) + getCredentials(opts: { + url: string; + }): Promise; +} + +// @public +export type AzureDevOpsCredentialType = 'bearer' | 'pat'; // @public export class AzureIntegration implements ScmIntegration { @@ -75,11 +117,13 @@ export class AzureIntegration implements ScmIntegration { export type AzureIntegrationConfig = { host: string; token?: string; - credential?: AzureCredential; + credential?: AzureDevOpsCredential; + credentials?: AzureDevOpsCredential[]; }; // @public -export type AzureManagedIdentityCredential = { +export type AzureManagedIdentityCredential = AzureCredentialBase & { + kind: 'ManagedIdentity'; clientId: string; }; @@ -180,6 +224,20 @@ export function buildGerritGitilesArchiveUrl( filePath: string, ): string; +// @public +export class DefaultAzureDevOpsCredentialsProvider + implements AzureDevOpsCredentialsProvider +{ + // (undocumented) + static fromIntegrations( + integrations: ScmIntegrationRegistry, + ): DefaultAzureDevOpsCredentialsProvider; + // (undocumented) + getCredentials(opts: { + url: string; + }): Promise; +} + // @public export class DefaultGithubCredentialsProvider implements GithubCredentialsProvider @@ -250,7 +308,7 @@ export function getAzureDownloadUrl(url: string): string; // @public export function getAzureFileFetchUrl(url: string): string; -// @public +// @public @deprecated export function getAzureRequestOptions( config: AzureIntegrationConfig, additionalHeaders?: Record, @@ -600,6 +658,12 @@ export function parseGerritGitilesUrl( // @public export function parseGerritJsonResponse(response: Response): Promise; +// @public +export type PersonalAccessTokenCredential = AzureCredentialBase & { + kind: 'PersonalAccessToken'; + personalAccessToken: string; +}; + // @public export function readAwsS3IntegrationConfig( config: Config, diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index a1eb7aceb9..9c08ec03a9 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -30,8 +30,38 @@ export interface Config { /** * Token used to authenticate requests. * @visibility secret + * @deprecated Use `credentials` instead. */ token?: string; + + /** + * The credential to use for requests. + * + * If no credential is specified anonymous access is used. + * + * @visibility secret + * @deprecated Use `credentials` instead. + */ + credential?: { + clientId?: string; + clientSecret?: string; + tenantId?: string; + personalAccessToken?: string; + }; + + /** + * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used. + * If not organization matches the first credential without an organization is used. + * + * If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used. + * @visibility secret + */ + credentials?: { + clientId?: string; + clientSecret?: string; + tenantId?: string; + personalAccessToken?: string; + }[]; }>; /** diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index d036d88c21..fe2cbf349c 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -29,6 +29,7 @@ export class AzureIntegration implements ScmIntegration { const configs = readAzureIntegrationConfigs( config.getOptionalConfigArray('integrations.azure') ?? [], ); + return basicIntegrations( configs.map(c => new AzureIntegration(c)), i => i.config.host, diff --git a/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.test.ts b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.test.ts new file mode 100644 index 0000000000..c9fbb4c324 --- /dev/null +++ b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.test.ts @@ -0,0 +1,188 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; +import { + AccessToken, + ClientSecretCredential, + ManagedIdentityCredential, +} from '@azure/identity'; + +const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< + typeof ClientSecretCredential +>; + +const MockedManagedIdentityCredential = + ManagedIdentityCredential as jest.MockedClass< + typeof ManagedIdentityCredential + >; + +jest.mock('@azure/identity'); + +const seconds = (s: number) => s * 1000; +const minutes = (m: number) => seconds(60) * m; +const hours = (h: number) => minutes(60) * h; + +describe('CachedAzureDevOpsCredentialsProvider', () => { + beforeEach(() => { + jest.resetAllMocks(); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + hours(8), + token: 'fake-client-secret-token', + } as AccessToken), + ); + + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + hours(8), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + }); + + it('Should return a pat credential when a personal access token is configured', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'PersonalAccessToken', + personalAccessToken: 'token', + }); + const { headers, type, token } = await manager.getCredentials(); + + expect(headers).toStrictEqual({ + Authorization: `Basic ${btoa(`:${token}`)}`, + }); + + expect(type).toBe('pat'); + expect(token).toBe('token'); + }); + + it('Should return a bearer credential when a client secret is configured', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }); + + const { headers, type, token } = await manager.getCredentials(); + + expect(headers).toStrictEqual({ + Authorization: `Bearer fake-client-secret-token`, + }); + + expect(type).toBe('bearer'); + expect(token).toBe('fake-client-secret-token'); + }); + + it('Should return a bearer credential when a managed identity is configured', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ManagedIdentity', + clientId: 'id', + }); + + const { headers, type, token } = await manager.getCredentials(); + + expect(headers).toStrictEqual({ + Authorization: `Bearer fake-managed-identity-token`, + }); + + expect(type).toBe('bearer'); + expect(token).toBe('fake-managed-identity-token'); + }); + + it('Should not refresh the client secret token when it does not expire within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }); + + await manager.getCredentials(); + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedClientSecretCredential.prototype.getToken, + ).toHaveBeenCalledTimes(1); + }); + + it('Should return the managed identity token when it does not expire within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ManagedIdentity', + clientId: 'id', + }); + + await manager.getCredentials(); + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedManagedIdentityCredential.prototype.getToken, + ).toHaveBeenCalledTimes(1); + }); + + it('Should refresh the client secret token when it expires within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + minutes(9) + seconds(59), + token: 'fake-client-secret-token', + } as AccessToken), + ); + + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedClientSecretCredential.prototype.getToken, + ).toHaveBeenCalledTimes(2); + }); + + it('Should refresh the managed identity token when it expires within 10 minutes', async () => { + const manager = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential({ + kind: 'ManagedIdentity', + clientId: 'id', + }); + + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: Date.now() + minutes(9) + seconds(59), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + + await manager.getCredentials(); + await manager.getCredentials(); + + expect( + MockedManagedIdentityCredential.prototype.getToken, + ).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.ts b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.ts new file mode 100644 index 0000000000..aee4cefc41 --- /dev/null +++ b/packages/integration/src/azure/CachedAzureDevOpsCredentialsProvider.ts @@ -0,0 +1,127 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AzureDevOpsCredential, PersonalAccessTokenCredential } from './config'; +import { + ClientSecretCredential, + ManagedIdentityCredential, + TokenCredential, +} from '@azure/identity'; +import { + AzureDevOpsCredentials, + AzureDevOpsCredentialsProvider, +} from './types'; + +type CachedAzureDevOpsCredentials = AzureDevOpsCredentials & { + expiresAt?: number; +}; + +function exhaustiveCheck(_param: never) {} + +const tenMinutes = 1000 * 60 * 10; + +/** + * A credentials provider that caches the credentials for as long as it is valid. + * + * @public + */ +export class CachedAzureDevOpsCredentialsProvider + implements AzureDevOpsCredentialsProvider +{ + azureDevOpsScope = '499b84ac-1321-427f-aa17-267ca6975798/.default'; + cached: CachedAzureDevOpsCredentials | undefined; + + static fromAzureDevOpsCredential( + credential: AzureDevOpsCredential, + ): CachedAzureDevOpsCredentialsProvider { + switch (credential.kind) { + case 'PersonalAccessToken': + return CachedAzureDevOpsCredentialsProvider.fromPersonalAccessTokenCredential( + credential, + ); + case 'ClientSecret': + return CachedAzureDevOpsCredentialsProvider.fromTokenCredential( + new ClientSecretCredential( + credential.tenantId, + credential.clientId, + credential.clientSecret, + ), + ); + case 'ManagedIdentity': + return CachedAzureDevOpsCredentialsProvider.fromTokenCredential( + new ManagedIdentityCredential(credential.clientId), + ); + default: + exhaustiveCheck(credential); + + throw new Error( + `Credential kind '${(credential as any).kind}' not supported`, + ); + } + } + + static fromTokenCredential( + credential: TokenCredential, + ): CachedAzureDevOpsCredentialsProvider { + return new CachedAzureDevOpsCredentialsProvider(credential); + } + + static fromPersonalAccessTokenCredential( + credential: PersonalAccessTokenCredential, + ) { + return new CachedAzureDevOpsCredentialsProvider( + credential.personalAccessToken, + ); + } + + private constructor(private readonly credential: TokenCredential | string) {} + + async getCredentials(): Promise { + if ( + this.cached === undefined || + (this.cached.expiresAt !== undefined && + Date.now() > this.cached.expiresAt) + ) { + if (typeof this.credential === 'string') { + this.cached = { + headers: { + Authorization: `Basic ${btoa(`:${this.credential}`)}`, + }, + type: 'pat', + token: this.credential, + }; + } else { + const accessToken = await this.credential.getToken( + this.azureDevOpsScope, + ); + + if (!accessToken) { + throw new Error('Failed to retrieve access token'); + } + + this.cached = { + expiresAt: accessToken.expiresOnTimestamp - tenMinutes, + headers: { + Authorization: `Bearer ${accessToken.token}`, + }, + type: 'bearer', + token: accessToken.token, + }; + } + } + + return this.cached; + } +} diff --git a/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.test.ts b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.test.ts new file mode 100644 index 0000000000..8f66d5742d --- /dev/null +++ b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.test.ts @@ -0,0 +1,465 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ScmIntegrations } from '../ScmIntegrations'; +import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; +import { AzureDevOpsCredentialLike, AzureIntegrationConfig } from './config'; +import { ConfigReader } from '@backstage/config'; +import { DefaultAzureDevOpsCredentialsProvider } from './DefaultAzureDevOpsCredentialsProvider'; +import { + AccessToken, + ClientSecretCredential, + DefaultAzureCredential, + ManagedIdentityCredential, +} from '@azure/identity'; +import { DateTime } from 'luxon'; + +type AzureIntegrationConfigLike = Partial< + Omit +> & { + credential?: Partial; + credentials?: Partial[]; +}; + +const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< + typeof ClientSecretCredential +>; + +const MockedManagedIdentityCredential = + ManagedIdentityCredential as jest.MockedClass< + typeof ManagedIdentityCredential + >; + +const MockedDefaultAzureCredential = DefaultAzureCredential as jest.MockedClass< + typeof DefaultAzureCredential +>; + +jest.mock('@azure/identity'); + +describe('DefaultAzureDevOpsCredentialProvider', () => { + const buildProvider = (azureIntegrations: AzureIntegrationConfigLike[]) => + DefaultAzureDevOpsCredentialsProvider.fromIntegrations( + ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: azureIntegrations, + }, + }), + ), + ); + + beforeEach(() => { + jest.resetAllMocks(); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-client-secret-token', + } as AccessToken), + ); + + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + + MockedDefaultAzureCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-default-azure-credential-token', + } as AccessToken), + ); + + jest.spyOn( + CachedAzureDevOpsCredentialsProvider, + 'fromAzureDevOpsCredential', + ); + + jest.spyOn(CachedAzureDevOpsCredentialsProvider, 'fromTokenCredential'); + + jest.spyOn( + CachedAzureDevOpsCredentialsProvider, + 'fromPersonalAccessTokenCredential', + ); + }); + + describe('fromIntegrations', () => { + it('Should create a credential provider when a credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'pat', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential, + ).toHaveBeenCalledTimes(1); + }); + + it('Should create a single credential provider per credential', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'client-id-1', + }, + { + organizations: ['org3', 'org4'], + clientId: 'client-id-2', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential, + ).toHaveBeenCalledTimes(2); + }); + + it('Should create a default credential provider for Azure DevOps when no credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(1); + + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledWith(new DefaultAzureCredential()); + }); + + it('Should create a default credential provider for Azure DevOps when no default credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'client-id', + tenantId: 'tenant-id', + clientSecret: 'client-secret', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(2); + + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledWith(new DefaultAzureCredential()); + }); + + it('Should not create a default credential provider for Azure DevOps when another default credential is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + clientId: 'client-id', + tenantId: 'tenant-id', + clientSecret: 'client-secret', + }, + ], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(1); + + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledWith(expect.any(ClientSecretCredential)); + }); + + it('Should not create a default credential provider for on-premise Azure DevOps server when no credential is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [], + }, + ]); + + expect(provider).toBeDefined(); + expect( + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential, + ).toHaveBeenCalledTimes(0); + + // expect 1 call because the Azure integration adds a default integration for dev.azure.com when it is not configured + expect( + CachedAzureDevOpsCredentialsProvider.fromTokenCredential, + ).toHaveBeenCalledTimes(1); + + expect( + CachedAzureDevOpsCredentialsProvider.fromPersonalAccessTokenCredential, + ).toHaveBeenCalledTimes(0); + }); + }); + + describe('getCredentials', () => { + describe('Azure DevOps (dev.azure.com)', () => { + it('Should return a token when a credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = provider.getCredentials({ + url: 'https://dev.azure.com/org1/project1', + }); + + expect(credentials).toBeDefined(); + }); + + it('Should return DefaultAzureCredential when no credential with the same organization is specified and host is dev.azure.com', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://dev.azure.com/org2/project1', + }); + + expect(credentials).toMatchObject({ + token: 'fake-default-azure-credential-token', + }); + }); + + it('Should return undefined when no credential is specified and host is Azure DevOps server', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://my.devops.server/org2/project1', + }); + + expect(credentials).toBeUndefined(); + }); + + it('Should prefer organization credential when a credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://dev.azure.com/org1/project1', + }); + + expect(credentials).toMatchObject({ + token: 'org1-pat', + }); + }); + + it('Should fallback to host credential when no credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: 'https://dev.azure.com/org2/project1', + }); + + expect(credentials).toMatchObject({ + token: 'fallback-pat', + }); + }); + }); + describe('Azure DevOps Server', () => { + [ + 'https://{host}/{organization}/{project}', + 'https://{host}/tfs/{organization}/{project}', + ].map(format => { + describe(`With url format ${format}`, () => { + const formatUrl = (opts: { + host: string; + organization: string; + project: string; + }) => + format + .replace('{host}', opts.host) + .replace('{organization}', opts.organization) + .replace('{project}', opts.project); + + it('Should return a token when a credential with the same host is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.devops.server', + organization: 'org1', + project: 'project1', + }), + }); + + expect(credentials).toBeDefined(); + }); + + it('Should prefer organization credential when a credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.devops.server', + organization: 'org1', + project: 'project1', + }), + }); + + expect(credentials).toMatchObject({ + token: 'org1-pat', + }); + }); + + it('Should fallback to host credential when no credential with the same organization is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'fallback-pat', + }, + { + organizations: ['org1'], + personalAccessToken: 'org1-pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.devops.server', + organization: 'org2', + project: 'project1', + }), + }); + + expect(credentials).toMatchObject({ + token: 'fallback-pat', + }); + }); + + it('Should return a undefined when no credential with the same host is specified', async () => { + const provider = buildProvider([ + { + host: 'my.devops.server', + credentials: [ + { + personalAccessToken: 'pat', + }, + ], + }, + ]); + + const credentials = await provider.getCredentials({ + url: formatUrl({ + host: 'my.other.devops.server', + organization: 'org1', + project: 'project1', + }), + }); + + expect(credentials).toBeUndefined(); + }); + }); + }); + }); + }); +}); diff --git a/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.ts b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.ts new file mode 100644 index 0000000000..1ff35b0612 --- /dev/null +++ b/packages/integration/src/azure/DefaultAzureDevOpsCredentialsProvider.ts @@ -0,0 +1,132 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + AzureDevOpsCredentials, + AzureDevOpsCredentialsProvider, +} from './types'; +import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; +import { ScmIntegrationRegistry } from '../registry'; +import { DefaultAzureCredential } from '@azure/identity'; + +/** + * Default implementation of AzureDevOpsCredentialsProvider. + * @public + */ +export class DefaultAzureDevOpsCredentialsProvider + implements AzureDevOpsCredentialsProvider +{ + static fromIntegrations( + integrations: ScmIntegrationRegistry, + ): DefaultAzureDevOpsCredentialsProvider { + const providers = integrations.azure.list().reduce((acc, integration) => { + integration.config.credentials?.forEach(credential => { + if ( + credential.organizations === undefined || + credential.organizations.length === 0 + ) { + if (acc.get(integration.config.host) === undefined) { + acc.set( + integration.config.host, + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( + credential, + ), + ); + } + } else { + const provider = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( + credential, + ); + credential.organizations?.forEach(organization => { + acc.set(`${integration.config.host}/${organization}`, provider); + }); + } + }); + + if ( + integration.config.host === 'dev.azure.com' && + acc.get(integration.config.host) === undefined + ) { + acc.set( + integration.config.host, + CachedAzureDevOpsCredentialsProvider.fromTokenCredential( + new DefaultAzureCredential(), + ), + ); + } + + return acc; + }, new Map()); + + return new DefaultAzureDevOpsCredentialsProvider(providers); + } + + private constructor( + private readonly providers: Map< + string, + CachedAzureDevOpsCredentialsProvider + >, + ) {} + + private forAzureDevOpsServerOrganization( + url: URL, + ): AzureDevOpsCredentialsProvider | undefined { + const parts = url.pathname.split('/').filter(part => part !== ''); + if (url.host !== 'dev.azure.com' && parts.length > 0) { + if (parts[0] !== 'tfs') { + // url format: https://{host}/{organization} + return this.providers.get(`${url.host}/${parts[0]}`); + } else if (parts[0] === 'tfs' && parts.length > 1) { + // url format: https://{host}/tfs/{organization} + return this.providers.get(`${url.host}/${parts[1]}`); + } + } + + return undefined; + } + + private forAzureDevOpsOrganization( + url: URL, + ): AzureDevOpsCredentialsProvider | undefined { + const parts = url.pathname.split('/').filter(part => part !== ''); + if (url.host === 'dev.azure.com' && parts.length > 0) { + // url format: https://{host}/{organization} + return this.providers.get(`${url.host}/${parts[0]}`); + } + + return undefined; + } + + private forHost(url: URL): AzureDevOpsCredentialsProvider | undefined { + return this.providers.get(url.host); + } + + async getCredentials(opts: { + url: string; + }): Promise { + const url = new URL(opts.url); + const provider = + this.forAzureDevOpsOrganization(url) ?? + this.forAzureDevOpsServerOrganization(url) ?? + this.forHost(url); + + if (provider === undefined) { + return undefined; + } + + return provider.getCredentials(opts); + } +} diff --git a/packages/integration/src/azure/config.test.ts b/packages/integration/src/azure/config.test.ts index 54711e4ddb..eb36120c32 100644 --- a/packages/integration/src/azure/config.test.ts +++ b/packages/integration/src/azure/config.test.ts @@ -17,18 +17,30 @@ import { Config, ConfigReader } from '@backstage/config'; import { loadConfigSchema } from '@backstage/config-loader'; import { + AzureDevOpsCredentialLike, AzureIntegrationConfig, readAzureIntegrationConfig, readAzureIntegrationConfigs, } from './config'; +type AzureIntegrationConfigLike = Partial< + Omit +> & { + credential?: Partial; + credentials?: Partial[]; +}; + describe('readAzureIntegrationConfig', () => { - function buildConfig(data: Partial): Config { + const valid: any = { + host: 'dev.azure.com', + }; + + function buildConfig(data: AzureIntegrationConfigLike): Config { return new ConfigReader(data); } async function buildFrontendConfig( - data: Partial, + data: AzureIntegrationConfigLike, ): Promise { const fullSchema = await loadConfigSchema({ dependencies: ['@backstage/integration'], @@ -51,39 +63,155 @@ describe('readAzureIntegrationConfig', () => { return new ConfigReader((processed[0].data as any).integrations.azure[0]); } - it('reads all values when using a token', () => { - const output = readAzureIntegrationConfig( - buildConfig({ - host: 'a.com', - token: 't', - }), - ); - - expect(output).toEqual({ - host: 'a.com', - token: 't', - }); - }); - - it('reads all values when using a credential', () => { + it('reads all values when using a personal access token credential', () => { const output = readAzureIntegrationConfig( buildConfig({ host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenant', - }, + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 't', + }, + ], }), ); expect(output).toEqual({ host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenant', - }, + credentials: [ + { + kind: 'PersonalAccessToken', + organizations: ['org1'], + personalAccessToken: 't', + }, + ], + }); + }); + + it('reads all values when using a personal access token credential (without organizations)', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'a.com', + credentials: [ + { + personalAccessToken: 't', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'a.com', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: 't', + }, + ], + }); + }); + + it('reads all values when using a client secret credential', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + organizations: ['org1', 'org2'], + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }); + }); + + it('reads all values when using a client secret credential (without organizations)', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }); + }); + + it('reads all values when using a managed identity credential', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }); + }); + + it('reads all values when using a managed identity credential (without organizations)', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + }, + ], + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + clientId: 'id', + }, + ], }); }); @@ -92,96 +220,296 @@ describe('readAzureIntegrationConfig', () => { expect(output).toEqual({ host: 'dev.azure.com' }); }); - it('rejects funky configs', () => { - const valid: any = { - host: 'dev.azure.com', - }; + it('maps deprecated token to credentials', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + token: 't', + }), + ); + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: 't', + }, + ], + }); + }); + + it('maps deprecated credential to credentials', () => { + const output = readAzureIntegrationConfig( + buildConfig({ + host: 'dev.azure.com', + credential: { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }, + }), + ); + + expect(output).toEqual({ + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenantId', + }, + ], + }); + }); + + it('rejects config when host is not valid', () => { expect(() => readAzureIntegrationConfig(buildConfig({ ...valid, host: 7 })), ).toThrow(/host/); + }); - expect(() => - readAzureIntegrationConfig(buildConfig({ ...valid, token: 7 })), - ).toThrow(/token/); - - expect(() => - readAzureIntegrationConfig(buildConfig({ ...valid, credential: 7 })), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { clientId: 7 } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { clientSecret: 7 } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { tenantId: 7 } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig(buildConfig({ ...valid, credential: {} })), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { clientSecret: 'secret' } }), - ), - ).toThrow(/credential/); - - expect(() => - readAzureIntegrationConfig( - buildConfig({ ...valid, credential: { tenantId: 'tenant' } }), - ), - ).toThrow(/credential/); - + it('rejects config when organizations is not valid', () => { expect(() => readAzureIntegrationConfig( buildConfig({ ...valid, - credential: { clientId: 'id', clientSecret: 'secret' }, + credentials: [ + { + organizations: [1, 2, 'org'], + }, + ], }), ), - ).toThrow(/credential/); + ).toThrow(/credentials/); + }); + it('rejects config when token is not valid', () => { expect(() => readAzureIntegrationConfig( buildConfig({ ...valid, - credential: { clientId: 'id', tenantId: 'tenant' }, + credentials: [ + { + personalAccessToken: 7, + }, + ], }), ), - ).toThrow(/credential/); + ).toThrow(/credentials/); + }); + it('rejects config when clientId is not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 7, + }, + ], + }), + ), + ).toThrow(/credentials/); + }); + + it('rejects config when clientSecret is not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 'id', + clientSecret: 7, + tenantId: 'tenant', + }, + ], + }), + ), + ).toThrow(/credentials/); + }); + + it('rejects config when tenantId is not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 7, + }, + ], + }), + ), + ).toThrow(/credentials/); + }); + + it('rejects config when at least one credential not valid', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + personalAccessToken: 'token', + }, + { + clientId: 'id', + }, + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + { + clientId: 'id', + personalAccessToken: 'token', + }, + ], + }), + ), + ).toThrow(/not a valid credential/); + }); + + it('rejects config when using a client secret credential for Azure DevOps server', () => { expect(() => readAzureIntegrationConfig( buildConfig({ ...valid, host: 'a.com', + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }), + ), + ).toThrow(/hosts/); + }); + + it('rejects config when using a managed identity for Azure DevOps server', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + host: 'a.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }), + ), + ).toThrow(/personal access tokens/); + }); + + it('rejects config when using organizations for Azure DevOps server', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + host: 'a.com', + credentials: [ + { + clientId: 'id', + }, + ], + }), + ), + ).toThrow(/hosts/); + }); + + it('rejects config when both the credential and credentials field are specified', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, credential: { clientId: 'id', - tenantId: 'tenant', - clientSecret: 'secret', }, + credentials: [ + { + clientId: 'id', + }, + ], }), ), ).toThrow(/credential/); }); + it('rejects config when both the token and credentials field are specified', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + token: 'token', + credentials: [ + { + clientId: 'id', + }, + ], + }), + ), + ).toThrow(/token/); + }); + + it('rejects config when more than one credential does not specify an organization', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + clientId: 'id', + }, + { + organizations: [], + personalAccessToken: 'pat', + }, + ], + }), + ), + ).toThrow(/organizations/); + }); + + it('rejects config when multiple credentials specify the same organization', () => { + expect(() => + readAzureIntegrationConfig( + buildConfig({ + ...valid, + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + { + organizations: ['org2', 'org3'], + personalAccessToken: 'pat', + }, + ], + }), + ), + ).toThrow(/organization org2/); + }); + it('works on the frontend', async () => { expect( readAzureIntegrationConfig( await buildFrontendConfig({ host: 'a.com', - token: 't', + credentials: [ + { + personalAccessToken: 't', + }, + ], }), ), ).toEqual({ @@ -191,46 +519,184 @@ describe('readAzureIntegrationConfig', () => { }); describe('readAzureIntegrationConfigs', () => { - function buildConfig(data: Partial[]): Config[] { + function buildConfig(data: AzureIntegrationConfigLike[]): Config[] { return data.map(item => new ConfigReader(item)); } - it('reads all values when using a token', () => { - const output = readAzureIntegrationConfigs( - buildConfig([ - { - host: 'a.com', - token: 't', - }, - ]), - ); - expect(output).toContainEqual({ - host: 'a.com', - token: 't', - }); - }); - - it('reads all values when using a credential', () => { + it('reads all values when using a personal access token credential', () => { const output = readAzureIntegrationConfigs( buildConfig([ { host: 'dev.azure.com', - credential: { + credentials: [ + { + organizations: ['org1'], + personalAccessToken: 't', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'PersonalAccessToken', + organizations: ['org1'], + personalAccessToken: 't', + }, + ], + }, + ]); + }); + + it('reads all values when using a personal access token credential (without organizations)', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + personalAccessToken: 't', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: 't', + }, + ], + }, + ]); + }); + + it('reads all values when using a client secret credential', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + organizations: ['org1', 'org2'], clientId: 'id', clientSecret: 'secret', tenantId: 'tenant', }, + ], + }, + ]); + }); + + it('reads all values when using a client secret credential (without organizations)', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], }, ]), ); - expect(output).toContainEqual({ - host: 'dev.azure.com', - credential: { - clientId: 'id', - clientSecret: 'secret', - tenantId: 'tenant', + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'id', + clientSecret: 'secret', + tenantId: 'tenant', + }, + ], }, - }); + ]); + }); + + it('reads all values when using a managed identity credential', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + organizations: ['org1', 'org2'], + clientId: 'id', + }, + ], + }, + ]); + }); + + it('reads all values when using a managed identity credential (without organizations)', () => { + const output = readAzureIntegrationConfigs( + buildConfig([ + { + host: 'dev.azure.com', + credentials: [ + { + clientId: 'id', + }, + ], + }, + ]), + ); + + expect(output).toEqual([ + { + host: 'dev.azure.com', + credentials: [ + { + kind: 'ManagedIdentity', + clientId: 'id', + }, + ], + }, + ]); }); it('adds a default entry when missing', () => { diff --git a/packages/integration/src/azure/config.ts b/packages/integration/src/azure/config.ts index 3f12f3ef13..98704ba570 100644 --- a/packages/integration/src/azure/config.ts +++ b/packages/integration/src/azure/config.ts @@ -36,6 +36,8 @@ export type AzureIntegrationConfig = { * The authorization token to use for requests. * * If no token is specified, anonymous access is used. + * + * @deprecated Use `credentials` instead. */ token?: string; @@ -43,15 +45,50 @@ export type AzureIntegrationConfig = { * The credential to use for requests. * * If no credential is specified anonymous access is used. + * + * @deprecated Use `credentials` instead. */ - credential?: AzureCredential; + credential?: AzureDevOpsCredential; + + /** + * The credentials to use for requests. If multiple credentials are specified the first one that matches the organization is used. + * If not organization matches the first credential without an organization is used. + * + * If no credentials are specified at all, either a default credential (for Azure DevOps) or anonymous access (for Azure DevOps Server) is used. + */ + credentials?: AzureDevOpsCredential[]; }; /** - * Authenticate using a client secret that was generated for an App Registration. + * The kind of Azure DevOps credential. * @public */ -export type AzureClientSecretCredential = { +export type AzureDevOpsCredentialKind = + | 'PersonalAccessToken' + | 'ClientSecret' + | 'ManagedIdentity'; + +/** + * Common fields for the Azure DevOps credentials. + * @public + */ +export type AzureCredentialBase = { + /** + * The kind of credential. + */ + kind: AzureDevOpsCredentialKind; + /** + * The Azure DevOps organizations for which to use this credential. + */ + organizations?: string[]; +}; + +/** + * A client secret credential that was generated for an App Registration. + * @public + */ +export type AzureClientSecretCredential = AzureCredentialBase & { + kind: 'ClientSecret'; /** * The Azure Active Directory tenant */ @@ -68,10 +105,11 @@ export type AzureClientSecretCredential = { }; /** - * Authenticate using a managed identity available at the deployment environment. + * A managed identity credential. * @public */ -export type AzureManagedIdentityCredential = { +export type AzureManagedIdentityCredential = AzureCredentialBase & { + kind: 'ManagedIdentity'; /** * The clientId */ @@ -79,33 +117,77 @@ export type AzureManagedIdentityCredential = { }; /** - * Credential used to authenticate to Azure Active Directory. + * A personal access token credential. * @public */ -export type AzureCredential = +export type PersonalAccessTokenCredential = AzureCredentialBase & { + kind: 'PersonalAccessToken'; + personalAccessToken: string; +}; + +/** + * The general shape of a credential that can be used to authenticate to Azure DevOps. + * @public + */ +export type AzureDevOpsCredentialLike = Omit< + Partial & + Partial & + Partial, + 'kind' +>; + +/** + * Credential used to authenticate to Azure DevOps. + * @public + */ +export type AzureDevOpsCredential = | AzureClientSecretCredential - | AzureManagedIdentityCredential; -export const isAzureClientSecretCredential = ( - credential: Partial, -): credential is AzureClientSecretCredential => { - const clientSecretCredential = credential as AzureClientSecretCredential; + | AzureManagedIdentityCredential + | PersonalAccessTokenCredential; - return ( - Object.keys(credential).length === 3 && - clientSecretCredential.clientId !== undefined && - clientSecretCredential.clientSecret !== undefined && - clientSecretCredential.tenantId !== undefined - ); -}; +const AzureDevOpsCredentialFields = [ + 'clientId', + 'clientSecret', + 'tenantId', + 'personalAccessToken', +] as const; +type AzureDevOpsCredentialField = (typeof AzureDevOpsCredentialFields)[number]; -export const isAzureManagedIdentityCredential = ( - credential: Partial, -): credential is AzureManagedIdentityCredential => { - return ( - Object.keys(credential).length === 1 && - (credential as AzureManagedIdentityCredential).clientId !== undefined - ); -}; +const AzureDevopsCredentialFieldMap = new Map< + AzureDevOpsCredentialKind, + AzureDevOpsCredentialField[] +>([ + ['ClientSecret', ['clientId', 'clientSecret', 'tenantId']], + ['ManagedIdentity', ['clientId']], + ['PersonalAccessToken', ['personalAccessToken']], +]); + +function asAzureDevOpsCredential( + credential: AzureDevOpsCredentialLike, +): AzureDevOpsCredential { + for (const entry of AzureDevopsCredentialFieldMap.entries()) { + const [kind, requiredFields] = entry; + + const forbiddenFields = AzureDevOpsCredentialFields.filter( + field => !requiredFields.includes(field as AzureDevOpsCredentialField), + ); + + if ( + requiredFields.every(field => credential[field] !== undefined) && + forbiddenFields.every(field => credential[field] === undefined) + ) { + return { + kind, + organizations: credential.organizations, + ...requiredFields.reduce((acc, field) => { + acc[field] = credential[field]; + return acc; + }, {} as Record), + } as AzureDevOpsCredential; + } + } + throw new Error('is not a valid credential'); +} /** * Reads a single Azure integration config. @@ -117,15 +199,62 @@ export function readAzureIntegrationConfig( config: Config, ): AzureIntegrationConfig { const host = config.getOptionalString('host') ?? AZURE_HOST; + + let credentialConfigs = config + .getOptionalConfigArray('credentials') + ?.map(credential => { + const result: Partial = { + organizations: credential.getOptionalStringArray('organizations'), + personalAccessToken: credential.getOptionalString( + 'personalAccessToken', + ), + tenantId: credential.getOptionalString('tenantId'), + clientId: credential.getOptionalString('clientId'), + clientSecret: credential.getOptionalString('clientSecret'), + }; + + return result; + }); + const token = config.getOptionalString('token'); - const credential = config.getOptional('credential') - ? { + if ( + config.getOptional('credential') !== undefined && + config.getOptional('credentials') !== undefined + ) { + throw new Error( + `Invalid Azure integration config, 'credential' and 'credentials' cannot be used together. Use 'credentials' instead.`, + ); + } + + if ( + config.getOptional('token') !== undefined && + config.getOptional('credentials') !== undefined + ) { + throw new Error( + `Invalid Azure integration config, 'token' and 'credentials' cannot be used together. Use 'credentials' instead.`, + ); + } + + if (token !== undefined) { + const mapped = [{ personalAccessToken: token }]; + credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped; + } + + if (config.getOptional('credential') !== undefined) { + const mapped = [ + { + organizations: config.getOptionalStringArray( + 'credential.organizations', + ), + token: config.getOptionalString('credential.token'), tenantId: config.getOptionalString('credential.tenantId'), clientId: config.getOptionalString('credential.clientId'), clientSecret: config.getOptionalString('credential.clientSecret'), - } - : undefined; + }, + ]; + credentialConfigs = credentialConfigs?.concat(mapped) ?? mapped; + } if (!isValidHost(host)) { throw new Error( @@ -133,29 +262,92 @@ export function readAzureIntegrationConfig( ); } - if ( - credential && - !isAzureClientSecretCredential(credential) && - !isAzureManagedIdentityCredential(credential) - ) { - throw new Error( - `Invalid Azure integration config, credential is not valid`, + let credentials: AzureDevOpsCredential[] | undefined = undefined; + if (credentialConfigs !== undefined) { + const errors = credentialConfigs + ?.reduce((acc, credentialConfig, index) => { + let error: string | undefined = undefined; + try { + asAzureDevOpsCredential(credentialConfig); + } catch (e) { + error = e.message; + } + + if (error !== undefined) { + acc.push(`credential at position ${index + 1} ${error}`); + } + + return acc; + }, Array.of()) + .concat( + Object.entries( + credentialConfigs + .filter( + credential => + credential.organizations !== undefined && + credential.organizations.length > 0, + ) + .reduce((acc, credential, index) => { + credential.organizations?.forEach(organization => { + if (!acc[organization]) { + acc[organization] = []; + } + + acc[organization].push(index + 1); + }); + + return acc; + }, {} as Record), + ) + .filter(([_, indexes]) => indexes.length > 1) + .reduce((acc, [org, indexes]) => { + acc.push( + `organization ${org} is specified multiple times in credentials at positions ${indexes + .slice(0, indexes.length - 1) + .join(', ')} and ${indexes[indexes.length - 1]}`, + ); + return acc; + }, Array.of()), + ); + + if (errors?.length > 0) { + throw new Error( + `Invalid Azure integration config for ${host}: ${errors.join('; ')}`, + ); + } + + credentials = credentialConfigs.map(credentialConfig => + asAzureDevOpsCredential(credentialConfig), ); + + if ( + credentials.some( + credential => credential.kind !== 'PersonalAccessToken', + ) && + host !== AZURE_HOST + ) { + throw new Error( + `Invalid Azure integration config for ${host}, only personal access tokens can be used with hosts other than ${AZURE_HOST}`, + ); + } + + if ( + credentials.filter( + credential => + credential.organizations === undefined || + credential.organizations.length === 0, + ).length > 1 + ) { + throw new Error( + `Invalid Azure integration config for ${host}, you cannot specify multiple credentials without organizations`, + ); + } } - if (credential && host !== AZURE_HOST) { - throw new Error( - `Invalid Azure integration config, credential can only be used with ${AZURE_HOST}`, - ); - } - - if (credential && token) { - throw new Error( - `Invalid Azure integration config, specify either a token or a credential but not both`, - ); - } - - return { host, token, credential }; + return { + host, + credentials, + }; } /** diff --git a/packages/integration/src/azure/core.test.ts b/packages/integration/src/azure/core.test.ts index e6ef5401ac..601057e499 100644 --- a/packages/integration/src/azure/core.test.ts +++ b/packages/integration/src/azure/core.test.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - getAzureFileFetchUrl, - getAzureDownloadUrl, - getAzureRequestOptions, -} from './core'; +import { getAzureFileFetchUrl, getAzureDownloadUrl } from './core'; import { AccessToken, ClientSecretCredential, @@ -44,66 +40,6 @@ MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => ); describe('azure core', () => { - describe('getAzureRequestOptions', () => { - it('should not add authorization header when not using token or credential', async () => { - expect(await getAzureRequestOptions({ host: '' })).toEqual( - expect.objectContaining({ - headers: expect.not.objectContaining({ - Authorization: expect.anything(), - }), - }), - ); - }); - - it('should add authorization header when using a personal access token', async () => { - expect( - await getAzureRequestOptions({ host: '', token: '0123456789' }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Basic OjAxMjM0NTY3ODk=', - }), - }), - ); - }); - - it('should add authorization header when using a client secret', async () => { - expect( - await getAzureRequestOptions({ - host: '', - credential: { - clientId: 'fake-id', - clientSecret: 'fake-secret', - tenantId: 'fake-tenant', - }, - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer fake-client-secret-token', - }), - }), - ); - }); - - it('should add authorization header when using a managed identity', async () => { - expect( - await getAzureRequestOptions({ - host: '', - credential: { - clientId: 'fake-id', - }, - }), - ).toEqual( - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Bearer fake-managed-identity-token', - }), - }), - ); - }); - }); - describe('getAzureFileFetchUrl', () => { it.each([ { diff --git a/packages/integration/src/azure/core.ts b/packages/integration/src/azure/core.ts index 31aa13d91f..1c25373962 100644 --- a/packages/integration/src/azure/core.ts +++ b/packages/integration/src/azure/core.ts @@ -15,15 +15,6 @@ */ import { AzureUrl } from './AzureUrl'; -import { - AzureIntegrationConfig, - isAzureManagedIdentityCredential, - isAzureClientSecretCredential, -} from './config'; -import { - ClientSecretCredential, - ManagedIdentityCredential, -} from '@azure/identity'; /** * Given a URL pointing to a file on a provider, returns a URL that is suitable @@ -62,45 +53,3 @@ export function getAzureDownloadUrl(url: string): string { export function getAzureCommitsUrl(url: string): string { return AzureUrl.fromRepoUrl(url).toCommitsUrl(); } - -/** - * Gets the request options necessary to make requests to a given provider. - * - * @param config - The relevant provider config - * @param additionalHeaders - Additional headers for the request - * @public - */ -export async function getAzureRequestOptions( - config: AzureIntegrationConfig, - additionalHeaders?: Record, -): Promise<{ headers: Record }> { - const azureDevOpsScope = '499b84ac-1321-427f-aa17-267ca6975798/.default'; - const headers: Record = additionalHeaders - ? { ...additionalHeaders } - : {}; - - const { token, credential } = config; - if (credential) { - if (isAzureClientSecretCredential(credential)) { - const servicePrincipal = new ClientSecretCredential( - credential.tenantId, - credential.clientId, - credential.clientSecret, - ); - - const accessToken = await servicePrincipal.getToken(azureDevOpsScope); - headers.Authorization = `Bearer ${accessToken.token}`; - } else if (isAzureManagedIdentityCredential(credential)) { - const managedIdentity = new ManagedIdentityCredential( - credential.clientId, - ); - const accessToken = await managedIdentity.getToken(azureDevOpsScope); - headers.Authorization = `Bearer ${accessToken.token}`; - } - } else if (token) { - const buffer = Buffer.from(`:${config.token}`, 'utf8'); - headers.Authorization = `Basic ${buffer.toString('base64')}`; - } - - return { headers }; -} diff --git a/packages/integration/src/azure/deprecated.test.ts b/packages/integration/src/azure/deprecated.test.ts new file mode 100644 index 0000000000..4411ef5f2f --- /dev/null +++ b/packages/integration/src/azure/deprecated.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { getAzureRequestOptions } from './deprecated'; +import { DateTime } from 'luxon'; +import { + AccessToken, + ClientSecretCredential, + ManagedIdentityCredential, +} from '@azure/identity'; + +jest.mock('@azure/identity'); + +const MockedClientSecretCredential = ClientSecretCredential as jest.MockedClass< + typeof ClientSecretCredential +>; + +const MockedManagedIdentityCredential = + ManagedIdentityCredential as jest.MockedClass< + typeof ManagedIdentityCredential + >; + +describe('azure core', () => { + beforeEach(() => { + jest.resetAllMocks(); + + MockedClientSecretCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-client-secret-token', + } as AccessToken), + ); + MockedManagedIdentityCredential.prototype.getToken.mockImplementation(() => + Promise.resolve({ + expiresOnTimestamp: DateTime.local().plus({ days: 1 }).toSeconds(), + token: 'fake-managed-identity-token', + } as AccessToken), + ); + }); + + describe('getAzureRequestOptions', () => { + it('should not add authorization header when not using token or credential', async () => { + expect(await getAzureRequestOptions({ host: '' })).toEqual( + expect.objectContaining({ + headers: expect.not.objectContaining({ + Authorization: expect.anything(), + }), + }), + ); + }); + + it('should add authorization header when using a personal access token', async () => { + const pat = '0123456789'; + const encoded = Buffer.from(`:${pat}`).toString('base64'); + expect( + await getAzureRequestOptions({ + host: '', + credentials: [ + { + kind: 'PersonalAccessToken', + personalAccessToken: pat, + }, + ], + }), + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: `Basic ${encoded}`, + }), + }), + ); + }); + + it('should add authorization header when using a client secret', async () => { + expect( + await getAzureRequestOptions({ + host: '', + credentials: [ + { + kind: 'ClientSecret', + clientId: 'fake-id', + clientSecret: 'fake-secret', + tenantId: 'fake-tenant', + }, + ], + }), + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer fake-client-secret-token', + }), + }), + ); + }); + + it('should add authorization header when using a managed identity', async () => { + expect( + await getAzureRequestOptions({ + host: '', + credentials: [ + { + kind: 'ManagedIdentity', + clientId: 'fake-id', + }, + ], + }), + ).toEqual( + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer fake-managed-identity-token', + }), + }), + ); + }); + }); +}); diff --git a/packages/integration/src/azure/deprecated.ts b/packages/integration/src/azure/deprecated.ts new file mode 100644 index 0000000000..fd19f3dc14 --- /dev/null +++ b/packages/integration/src/azure/deprecated.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AzureIntegrationConfig } from './config'; +import { CachedAzureDevOpsCredentialsProvider } from './CachedAzureDevOpsCredentialsProvider'; + +/** + * Gets the request options necessary to make requests to a given provider. + * + * @param config - The relevant provider config + * @param additionalHeaders - Additional headers for the request + * @public + * @deprecated Use {@link AzureDevOpsCredentialsProvider} instead. + */ +export async function getAzureRequestOptions( + config: AzureIntegrationConfig, + additionalHeaders?: Record, +): Promise<{ headers: Record }> { + const headers: Record = additionalHeaders + ? { ...additionalHeaders } + : {}; + + /* + * Since we do not have a way to determine which organization the request is for, + * we will use the first credential that does not have an organization specified. + */ + const credentialConfig = config.credentials?.filter( + credential => + credential.organizations === undefined || + credential.organizations.length === 0, + )[0]; + + if (credentialConfig) { + const credentialsProvider = + CachedAzureDevOpsCredentialsProvider.fromAzureDevOpsCredential( + credentialConfig, + ); + const credentials = await credentialsProvider.getCredentials(); + + return { + headers: { + ...credentials?.headers, + ...headers, + }, + }; + } + + return { headers }; +} diff --git a/packages/integration/src/azure/index.ts b/packages/integration/src/azure/index.ts index 21cf069807..b5f5c0a316 100644 --- a/packages/integration/src/azure/index.ts +++ b/packages/integration/src/azure/index.ts @@ -21,13 +21,21 @@ export { } from './config'; export type { AzureIntegrationConfig, - AzureCredential, - AzureManagedIdentityCredential, + AzureDevOpsCredentialKind, + AzureCredentialBase, AzureClientSecretCredential, + AzureManagedIdentityCredential, + PersonalAccessTokenCredential, + AzureDevOpsCredentialLike, + AzureDevOpsCredential, } from './config'; export { getAzureCommitsUrl, getAzureDownloadUrl, getAzureFileFetchUrl, - getAzureRequestOptions, } from './core'; + +export * from './types'; +export { DefaultAzureDevOpsCredentialsProvider } from './DefaultAzureDevOpsCredentialsProvider'; + +export * from './deprecated'; diff --git a/packages/integration/src/azure/types.ts b/packages/integration/src/azure/types.ts new file mode 100644 index 0000000000..72b341432c --- /dev/null +++ b/packages/integration/src/azure/types.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The type of Azure DevOps credential, either bearer or pat. + * @public + */ +export type AzureDevOpsCredentialType = 'bearer' | 'pat'; + +/** + * A set of credentials for Azure DevOps. + * + * @public + */ +export type AzureDevOpsCredentials = { + headers: { [name: string]: string }; + token: string; + type: AzureDevOpsCredentialType; +}; + +/** + * This allows implementations to be provided to retrieve Azure DevOps credentials. + * + * @public + * + */ +export interface AzureDevOpsCredentialsProvider { + getCredentials(opts: { + url: string; + }): Promise; +} diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index c00bae384f..6f6a8ed58f 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -39,6 +39,14 @@ describe('gerrit core', () => { host: 'gerrit.com', gitilesBaseUrl: 'https://gerrit.com/gitiles', }; + const configWithPath: GerritIntegrationConfig = { + host: 'gerrit.com', + gitilesBaseUrl: 'https://gerrit.com/path/gitiles', + }; + const configWithPathSlash: GerritIntegrationConfig = { + host: 'gerrit.com', + gitilesBaseUrl: 'https://gerrit.com/path1/path2/gitiles/', + }; it('can create an archive url for a branch', () => { expect(buildGerritGitilesArchiveUrl(config, 'repo', 'dev', '')).toEqual( 'https://gerrit.com/gitiles/repo/+archive/refs/heads/dev.tar.gz', @@ -67,6 +75,30 @@ describe('gerrit core', () => { 'https://gerrit.com/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', ); }); + it('can create an authenticated url when auth is enabled and an url-path is used', () => { + const authConfig = { + ...configWithPath, + username: 'username', + password: 'password', + }; + expect( + buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), + ).toEqual( + 'https://gerrit.com/path/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', + ); + }); + it('can create an authenticated url when auth is enabled and an url-path + slash is used', () => { + const authConfig = { + ...configWithPathSlash, + username: 'username', + password: 'password', + }; + expect( + buildGerritGitilesArchiveUrl(authConfig, 'repo', 'dev', 'docs'), + ).toEqual( + 'https://gerrit.com/path1/path2/a/gitiles/repo/+archive/refs/heads/dev/docs.tar.gz', + ); + }); }); describe('buildGerritGitilesUrl', () => { diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index 107d5a4469..e104ffc7fb 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -157,9 +157,13 @@ export function getGitilesAuthenticationUrl( config: GerritIntegrationConfig, ): string { const parsedUrl = new URL(config.gitilesBaseUrl!); - return `${parsedUrl.protocol}//${parsedUrl.host}${getAuthenticationPrefix( - config, - )}${parsedUrl.pathname.substring(1)}`; + parsedUrl.pathname = parsedUrl.pathname.replace(/\/?$/, ''); + parsedUrl.pathname = parsedUrl.pathname.replace( + /\/([^\/]*)$/, + `${getAuthenticationPrefix(config)}$1`, + ); + + return parsedUrl.toString(); } /** diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index 9c87bd8dff..12d71f3be4 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -44,6 +44,7 @@ Usage: backstage-repo-tools generate-catalog-info [options] Options: --dry-run + --ci -h, --help ``` diff --git a/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts b/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts index f485e872e9..25703e4fc2 100644 --- a/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts +++ b/packages/repo-tools/src/commands/generate-catalog-info/codeowners.ts @@ -59,7 +59,7 @@ export function getOwnerFromCodeowners( ): string { const relPath = relativePath('.', absPath); const possibleOwners = getPossibleCodeowners(codeowners, relPath); - const owner = possibleOwners.slice(-1)[0]; + const owner = possibleOwners[0]; if (!owner) { throw new Error(`${relPath} isn't owned by anyone in CODEOWNERS`); diff --git a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts index 08f73898c8..38bc3ab4b2 100644 --- a/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts +++ b/packages/repo-tools/src/commands/generate-catalog-info/generate-catalog-info.ts @@ -29,26 +29,34 @@ import { import { BackstagePackageJson, isBackstagePackage, + isRejected, + isFulfilled, readFile, writeFile, } from './utils'; import { CodeOwnersEntry } from 'codeowners-utils'; type CreateFixPackageInfoYamlsOptions = { + ci?: boolean; dryRun?: boolean; }; export default async (opts: CreateFixPackageInfoYamlsOptions) => { - const { dryRun = false } = opts; + const { dryRun = false, ci = false } = opts; const packages = await PackageGraph.listTargetPackages(); const codeowners = await loadCodeowners(); const limit = pLimit(10); - const results = await Promise.allSettled( + // If --ci is passed, don't make any changes to the file system; we're only + // interested in knowing if changes would have been made. + const isDryRun = ci ? true : dryRun; + const checkForChanges = ci; + + const results = await Promise.allSettled( packages.map(({ packageJson, dir }) => limit(async () => { if (!isBackstagePackage(packageJson)) { - return; + return ''; } // Check if there is already a corresponding catalog-info.yaml @@ -59,32 +67,30 @@ export default async (opts: CreateFixPackageInfoYamlsOptions) => { yamlString = await readFile(infoYamlPath, { encoding: 'utf-8' }); } catch (e) { if (e.code === 'ENOENT') { - await createCatalogInfoYaml({ + return await createCatalogInfoYaml({ yamlPath: infoYamlPath, packageJson, codeowners, - dryRun, + dryRun: isDryRun, }); - return; } throw e; } - await fixCatalogInfoYaml({ + return await fixCatalogInfoYaml({ yamlPath: infoYamlPath, packageJson, codeowners, yamlString, - dryRun, + dryRun: isDryRun, + ci, }); }), ), ); - const rejects = results.filter( - r => r.status === 'rejected', - ) as PromiseRejectedResult[]; + const rejects = results.filter(isRejected); if (rejects.length > 0) { // Problems encountered. Print details here. console.error( @@ -94,6 +100,25 @@ export default async (opts: CreateFixPackageInfoYamlsOptions) => { console.error(); process.exit(1); } + + if (checkForChanges) { + const instructions = results + .filter(isFulfilled) + .map(r => r.value) + .filter(r => r !== ''); + + // Non-empty instructions indicate changes would have been made. + if (instructions.length > 0) { + console.error( + '\ncatalog-info.yaml file(s) out of sync with CODEOWNERS and/or package.json (see instructions above)\n', + ); + process.exit(1); + } else { + console.error( + 'catalog-info.yaml file(s) in sync with CODEOWNERS and package.json', + ); + } + } }; type CreateOptions = { @@ -105,6 +130,7 @@ type CreateOptions = { type FixOptions = CreateOptions & { yamlString: string; + ci: boolean; }; type BackstagePackageEntity = Entity & { @@ -117,18 +143,23 @@ type BackstagePackageEntity = Entity & { }; }; -function createCatalogInfoYaml(options: CreateOptions) { +async function createCatalogInfoYaml(options: CreateOptions) { const { codeowners, dryRun, packageJson, yamlPath } = options; + const instruction = `Create ${relativePath('.', yamlPath)}`; const owner = getOwnerFromCodeowners(codeowners, yamlPath); const entity = createOrMergeEntity(packageJson, owner); - return dryRun - ? Promise.resolve(console.error(`Create ${relativePath('.', yamlPath)}`)) - : writeFile(yamlPath, YAML.dump(entity)); + if (dryRun) { + console.error(instruction); + } else { + await writeFile(yamlPath, YAML.dump(entity)); + } + + return instruction; } -function fixCatalogInfoYaml(options: FixOptions) { - const { codeowners, dryRun, packageJson, yamlPath, yamlString } = options; +async function fixCatalogInfoYaml(options: FixOptions) { + const { ci, codeowners, dryRun, packageJson, yamlPath, yamlString } = options; const possibleOwners = getPossibleCodeowners( codeowners, relativePath('.', yamlPath), @@ -149,19 +180,55 @@ function fixCatalogInfoYaml(options: FixOptions) { const badName = yamlJson.metadata.name !== safeName; const badType = yamlJson.spec?.type !== `backstage-${packageJson.backstage.role}`; - const badDesc = yamlJson.metadata.description !== packageJson.description; - if (badOwner || badTitle || badName || badType || badDesc) { + if (badOwner || badTitle || badName || badType) { const owner = badOwner ? getOwnerFromCodeowners(codeowners, yamlPath) : yamlJson.spec?.owner; const newJson = createOrMergeEntity(packageJson, owner, yamlJson); - return dryRun - ? Promise.resolve(console.error(`Update ${relativePath('.', yamlPath)}`)) - : writeFile(yamlPath, yamlOverwrite(yamlString, newJson)); + const instructions = [`Update ${relativePath('.', yamlPath)}`]; + + // Show more detailed instructions when --ci flag is set. + if (ci) { + if (badOwner) { + instructions.push( + ` spec.owner cannot be "${ + yamlJson.spec.owner + }" because it must be one of (${possibleOwners.join( + ', ', + )}) as listed in CODEOWNERS`, + ); + } + + if (badTitle) { + instructions.push( + ` metadata.title cannot be "${yamlJson.metadata.title}" because it must be exactly "${packageJson.name}", the package.json name`, + ); + } + + if (badName) { + instructions.push( + ` metadata.name cannot be "${yamlJson.metadata.name}" because it must be exactly "${safeName}", as derived from package.json name`, + ); + } + + if (badType) { + instructions.push( + ` spec.type cannot be "${yamlJson.spec.type}" because it must be exactly "backstage-${packageJson.backstage.role}", as derived from package.json backstage.role`, + ); + } + } + + if (dryRun) { + console.error(instructions.join('\n')); + } else { + await writeFile(yamlPath, yamlOverwrite(yamlString, newJson)); + } + + return instructions.join('\n'); } - return Promise.resolve(); + return ''; } /** diff --git a/packages/repo-tools/src/commands/generate-catalog-info/utils.ts b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts index f446f4e867..b00fbd57a8 100644 --- a/packages/repo-tools/src/commands/generate-catalog-info/utils.ts +++ b/packages/repo-tools/src/commands/generate-catalog-info/utils.ts @@ -40,3 +40,11 @@ export function isBackstagePackage( (packageJson as any)?.backstage?.role !== 'undefined' ); } + +export const isRejected = ( + input: PromiseSettledResult, +): input is PromiseRejectedResult => input.status === 'rejected'; + +export const isFulfilled = ( + input: PromiseSettledResult, +): input is PromiseFulfilledResult => input.status === 'fulfilled'; diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 7aa574d291..c429a253f8 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -102,6 +102,10 @@ export function registerCommands(program: Command) { '--dry-run', 'Shows what would happen without actually writing any yaml.', ) + .option( + '--ci', + 'CI run checks that there are no changes to catalog-info.yaml files', + ) .description('Create or fix info yaml files for all backstage packages') .action( lazy(() => diff --git a/packages/test-utils/alpha-api-report.md b/packages/test-utils/alpha-api-report.md index 3e0c4aa6c2..c37d79279c 100644 --- a/packages/test-utils/alpha-api-report.md +++ b/packages/test-utils/alpha-api-report.md @@ -4,11 +4,12 @@ ```ts import { PropsWithChildren } from 'react'; +import { default as React_2 } from 'react'; // @alpha export const MockPluginProvider: ({ children, -}: PropsWithChildren<{}>) => JSX.Element; +}: PropsWithChildren<{}>) => React_2.JSX.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 01fc31c28f..d4b38a243e 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -26,6 +26,7 @@ import { JsonValue } from '@backstage/types'; import { MatcherFunction } from '@testing-library/react'; import { Observable } from '@backstage/types'; import { PermissionApi } from '@backstage/plugin-permission-react'; +import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RenderOptions } from '@testing-library/react'; @@ -214,7 +215,7 @@ export type SyncLogCollector = () => void; // @public export const TestApiProvider: ( props: TestApiProviderProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type TestApiProviderProps = { diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md index 4cec5047fc..6ed7b68196 100644 --- a/plugins/adr/api-report.md +++ b/plugins/adr/api-report.md @@ -12,6 +12,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; import { isAdrAvailable } from '@backstage/plugin-adr-common'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -76,7 +78,10 @@ export const adrPlugin: BackstagePlugin< // @public export const AdrReader: { - (props: { adr: string; decorators?: AdrContentDecorator[] }): JSX.Element; + (props: { + adr: string; + decorators?: AdrContentDecorator[]; + }): React_2.JSX.Element; decorators: Readonly<{ createRewriteRelativeLinksDecorator(): AdrContentDecorator; createRewriteRelativeEmbedsDecorator(): AdrContentDecorator; @@ -107,7 +112,7 @@ export type AdrSearchResultListItemProps = { export const EntityAdrContent: (props: { contentDecorators?: AdrContentDecorator[] | undefined; filePathFilterFn?: AdrFilePathFilterFn | undefined; -}) => JSX.Element; +}) => JSX_2.Element; export { isAdrAvailable }; ``` diff --git a/plugins/airbrake/api-report.md b/plugins/airbrake/api-report.md index 773e2a029a..a04d57d7e9 100644 --- a/plugins/airbrake/api-report.md +++ b/plugins/airbrake/api-report.md @@ -3,10 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -19,7 +18,7 @@ export const airbrakePlugin: BackstagePlugin< >; // @public -export const EntityAirbrakeContent: () => JSX.Element; +export const EntityAirbrakeContent: () => React_2.JSX.Element; // @public export const isAirbrakeAvailable: (entity: Entity) => boolean; diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index 5ccd8bd076..e7fa7f0015 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -55,7 +56,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/allure/api-report.md b/plugins/allure/api-report.md index c1ce499ab2..a192851f26 100644 --- a/plugins/allure/api-report.md +++ b/plugins/allure/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -22,7 +23,7 @@ export const allurePlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityAllureReportContent: () => JSX.Element; +export const EntityAllureReportContent: () => JSX_2.Element; // @public (undocumented) export const isAllureReportAvailable: (entity: Entity) => boolean; diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 881d4d0c9d..b2737545fe 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -54,7 +55,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/apache-airflow/api-report.md b/plugins/apache-airflow/api-report.md index b427a0148e..be2a208bd4 100644 --- a/plugins/apache-airflow/api-report.md +++ b/plugins/apache-airflow/api-report.md @@ -6,15 +6,16 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public export const ApacheAirflowDagTable: (props: { dagIds?: string[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const ApacheAirflowPage: () => JSX.Element; +export const ApacheAirflowPage: () => JSX_2.Element; // @public (undocumented) export const apacheAirflowPlugin: BackstagePlugin< diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index 281d49c781..75c8e8b482 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -33,6 +33,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "qs": "^6.10.1", "react-use": "^17.2.4" @@ -52,7 +53,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/api-docs-module-protoc-gen-doc/api-report.md b/plugins/api-docs-module-protoc-gen-doc/api-report.md index 696fee2a20..836f8313d8 100644 --- a/plugins/api-docs-module-protoc-gen-doc/api-report.md +++ b/plugins/api-docs-module-protoc-gen-doc/api-report.md @@ -3,12 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// +import { default as React_2 } from 'react'; // @public (undocumented) export const grpcDocsApiWidget: { type: string; title: string; - component: (definition: string) => JSX.Element; + component: (definition: string) => React_2.JSX.Element; }; ``` diff --git a/plugins/api-docs-module-protoc-gen-doc/package.json b/plugins/api-docs-module-protoc-gen-doc/package.json index 2d79af78a3..3ae21fec48 100644 --- a/plugins/api-docs-module-protoc-gen-doc/package.json +++ b/plugins/api-docs-module-protoc-gen-doc/package.json @@ -31,6 +31,7 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "grpc-docs": "^1.1.2" }, "peerDependencies": { @@ -40,8 +41,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", - "@testing-library/jest-dom": "^5.16.4", - "@types/react": "^16.13.1 || ^17.0.0" + "@testing-library/jest-dom": "^5.16.4" }, "files": [ "dist" diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index a9b21664c3..59d490a1fe 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -11,6 +11,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogTableRow } from '@backstage/plugin-catalog'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableColumn } from '@backstage/core-components'; @@ -18,14 +19,14 @@ import { TableProps } from '@backstage/core-components'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; // @public (undocumented) -export const ApiDefinitionCard: () => JSX.Element; +export const ApiDefinitionCard: () => React_2.JSX.Element; // @public export function ApiDefinitionDialog(props: { open: boolean; entity: ApiEntity; onClose: () => void; -}): JSX.Element; +}): React_2.JSX.Element; // @public (undocumented) export type ApiDefinitionWidget = { @@ -62,20 +63,22 @@ export { apiDocsPlugin as plugin }; // @public export const ApiExplorerIndexPage: ( props: DefaultApiExplorerPageProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export const ApiExplorerPage: ( props: DefaultApiExplorerPageProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) -export const ApiTypeTitle: (props: { apiEntity: ApiEntity }) => JSX.Element; +export const ApiTypeTitle: (props: { + apiEntity: ApiEntity; +}) => React_2.JSX.Element; // @public (undocumented) export const AsyncApiDefinitionWidget: ( props: AsyncApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type AsyncApiDefinitionWidgetProps = { @@ -86,17 +89,17 @@ export type AsyncApiDefinitionWidgetProps = { export const ConsumedApisCard: (props: { variant?: InfoCardVariants; columns?: TableColumn[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const ConsumingComponentsCard: (props: { variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export const DefaultApiExplorerPage: ( props: DefaultApiExplorerPageProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type DefaultApiExplorerPageProps = { @@ -109,40 +112,40 @@ export type DefaultApiExplorerPageProps = { export function defaultDefinitionWidgets(): ApiDefinitionWidget[]; // @public (undocumented) -export const EntityApiDefinitionCard: () => JSX.Element; +export const EntityApiDefinitionCard: () => JSX_2.Element; // @public (undocumented) export const EntityConsumedApisCard: (props: { variant?: InfoCardVariants | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityConsumingComponentsCard: (props: { variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityHasApisCard: (props: { variant?: InfoCardVariants | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityProvidedApisCard: (props: { variant?: InfoCardVariants | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityProvidingComponentsCard: (props: { variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const GraphQlDefinitionWidget: ( props: GraphQlDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type GraphQlDefinitionWidgetProps = { @@ -153,12 +156,12 @@ export type GraphQlDefinitionWidgetProps = { export const HasApisCard: (props: { variant?: InfoCardVariants; columns?: TableColumn[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const OpenApiDefinitionWidget: ( props: OpenApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type OpenApiDefinitionWidgetProps = { @@ -168,7 +171,7 @@ export type OpenApiDefinitionWidgetProps = { // @public (undocumented) export const PlainApiDefinitionWidget: ( props: PlainApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type PlainApiDefinitionWidgetProps = { @@ -180,17 +183,17 @@ export type PlainApiDefinitionWidgetProps = { export const ProvidedApisCard: (props: { variant?: InfoCardVariants; columns?: TableColumn[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const ProvidingComponentsCard: (props: { variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const TrpcApiDefinitionWidget: ( props: TrpcApiDefinitionWidgetProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type TrpcApiDefinitionWidgetProps = { diff --git a/plugins/apollo-explorer/api-report.md b/plugins/apollo-explorer/api-report.md index 66cc473d0b..2dd6c14a90 100644 --- a/plugins/apollo-explorer/api-report.md +++ b/plugins/apollo-explorer/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { JSONObject } from '@apollo/explorer/src/helpers/types'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -30,7 +31,7 @@ export const ApolloExplorerPage: (props: { } | undefined; }[]; -}) => JSX.Element; +}) => JSX_2.Element; // @public export const apolloExplorerPlugin: BackstagePlugin< diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json index aae09fe19a..9b7aa372d5 100644 --- a/plugins/apollo-explorer/package.json +++ b/plugins/apollo-explorer/package.json @@ -34,6 +34,7 @@ "@backstage/theme": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4", "use-deep-compare-effect": "^1.8.1" }, @@ -52,7 +53,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/auth-backend-module-google-provider/README.md b/plugins/auth-backend-module-google-provider/README.md index e031482f64..633dd13717 100644 --- a/plugins/auth-backend-module-google-provider/README.md +++ b/plugins/auth-backend-module-google-provider/README.md @@ -1,5 +1,8 @@ -# Auth Backend Module - Google Provider +# Auth Module: Google Provider + +This module provides a Google auth provider implementation for `@backstage/plugin-auth-backend`. ## Links -- [The Backstage homepage](https://backstage.io) +- [Backstage](https://backstage.io) +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-google-provider) diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index 2104af5124..5c18717efe 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -40,6 +40,7 @@ "devDependencies": { "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", "@types/passport-google-oauth20": "^2.0.3", "msw": "^1.0.0", "supertest": "^6.1.3" diff --git a/plugins/auth-backend-module-google-provider/src/module.test.ts b/plugins/auth-backend-module-google-provider/src/module.test.ts new file mode 100644 index 0000000000..d186064230 --- /dev/null +++ b/plugins/auth-backend-module-google-provider/src/module.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authPlugin } from '@backstage/plugin-auth-backend'; +import { authModuleGoogleProvider } from './module'; +import request from 'supertest'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; + +describe('authModuleGoogleProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleGoogleProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + google: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get('/api/auth/google/start?env=development'); + + expect(res.status).toBe(302); + + const nonceCookie = agent.jar.getCookie('google-nonce', { + domain: 'localhost', + path: '/api/auth/google/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://accounts.google.com'); + expect(startUrl.pathname).toBe('/o/oauth2/v2/auth'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + access_type: 'offline', + prompt: 'consent', + response_type: 'code', + client_id: 'my-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/google/handler/frame`, + state: expect.any(String), + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); +}); diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 5eb038c436..9c5249f733 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -10,6 +10,7 @@ import { AuthProviderFactory as AuthProviderFactory_2 } from '@backstage/plugin- import { AuthProviderRouteHandlers as AuthProviderRouteHandlers_2 } from '@backstage/plugin-auth-node'; import { AuthResolverCatalogUserQuery as AuthResolverCatalogUserQuery_2 } from '@backstage/plugin-auth-node'; import { AuthResolverContext as AuthResolverContext_2 } from '@backstage/plugin-auth-node'; +import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageSignInResult } from '@backstage/plugin-auth-node'; import { CacheService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; @@ -51,6 +52,9 @@ export type AuthHandlerResult = { profile: ProfileInfo; }; +// @public +export const authPlugin: () => BackendFeature; + // @public @deprecated (undocumented) export type AuthProviderConfig = AuthProviderConfig_2; @@ -658,6 +662,8 @@ export interface RouterOptions { // (undocumented) database: PluginDatabaseManager; // (undocumented) + disableDefaultProviderFactories?: boolean; + // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) logger: LoggerService; diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 4ebf7c9de1..ce7ebc5204 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -31,6 +31,16 @@ export interface Config { secret?: string; }; + /** + * JWS "alg" (Algorithm) Header Parameter value. Defaults to ES256. + * Must match one of the algorithms defined for IdentityClient. + * When setting a different algorithm, check if the `key` field + * of the `signing_keys` table can fit the length of the generated keys. + * If not, add a knex migration file in the migrations folder. + * More info on supported algorithms: https://github.com/panva/jose + */ + identityTokenAlgorithm?: string; + /** To control how to store JWK data in auth-backend */ keyStore?: { provider?: 'database' | 'memory' | 'firestore'; diff --git a/plugins/auth-backend/dev/index.ts b/plugins/auth-backend/dev/index.ts new file mode 100644 index 0000000000..4d295bdc77 --- /dev/null +++ b/plugins/auth-backend/dev/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackend } from '@backstage/backend-defaults'; +import { authPlugin } from '../src'; +import { authModuleGoogleProvider } from '@backstage/plugin-auth-backend-module-google-provider'; + +const backend = createBackend(); + +backend.add(authPlugin); +backend.add(authModuleGoogleProvider); + +backend.start(); diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index c8d1633a47..e1500d6516 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -41,6 +41,7 @@ "@backstage/plugin-auth-backend-module-gcp-iap-provider": "workspace:^", "@backstage/plugin-auth-backend-module-google-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", "@davidzemon/passport-okta-oauth": "^0.0.5", "@google-cloud/firestore": "^6.0.0", @@ -80,6 +81,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-defaults": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/body-parser": "^1.19.0", diff --git a/plugins/auth-backend/src/authPlugin.test.ts b/plugins/auth-backend/src/authPlugin.test.ts new file mode 100644 index 0000000000..d8a15eba3e --- /dev/null +++ b/plugins/auth-backend/src/authPlugin.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import request from 'supertest'; +import { authPlugin } from './authPlugin'; + +describe('authPlugin', () => { + it('should provide an OpenID configuration', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + }, + }), + ], + }); + + const res = await request(server).get( + '/api/auth/.well-known/openid-configuration', + ); + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ + claims_supported: ['sub'], + issuer: `http://localhost:${server.port()}/api/auth`, + }); + }); +}); diff --git a/plugins/auth-backend/src/authPlugin.ts b/plugins/auth-backend/src/authPlugin.ts new file mode 100644 index 0000000000..0522424a99 --- /dev/null +++ b/plugins/auth-backend/src/authPlugin.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { + AuthProviderFactory, + authProvidersExtensionPoint, +} from '@backstage/plugin-auth-node'; +import { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha'; +import { createRouter } from './service/router'; + +/** + * Auth plugin + * + * @public + */ +export const authPlugin = createBackendPlugin({ + pluginId: 'auth', + register(reg) { + const providers = new Map(); + + reg.registerExtensionPoint(authProvidersExtensionPoint, { + registerProvider({ providerId, factory }) { + if (providers.has(providerId)) { + throw new Error( + `Auth provider '${providerId}' was already registered`, + ); + } + providers.set(providerId, factory); + }, + }); + + reg.registerInit({ + deps: { + httpRouter: coreServices.httpRouter, + logger: coreServices.logger, + config: coreServices.rootConfig, + database: coreServices.database, + discovery: coreServices.discovery, + tokenManager: coreServices.tokenManager, + catalogApi: catalogServiceRef, + }, + async init({ + httpRouter, + logger, + config, + database, + discovery, + tokenManager, + catalogApi, + }) { + const router = await createRouter({ + logger, + config, + database, + discovery, + tokenManager, + catalogApi, + providerFactories: Object.fromEntries(providers), + disableDefaultProviderFactories: true, + }); + httpRouter.use(router); + }, + }); + }, +}); diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index f8a8580359..d8323e02ee 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -20,6 +20,7 @@ * @packageDocumentation */ +export { authPlugin } from './authPlugin'; export * from './service/router'; export type { TokenParams } from './identity'; export * from './providers'; diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts index 37f10ff184..3b8eca0a95 100644 --- a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts +++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthHandler.ts @@ -37,7 +37,7 @@ export function adaptLegacyOAuthHandler( scope: result.session.scope, id_token: result.session.idToken, token_type: result.session.tokenType, - expires_in: result.session.expiresInSeconds, + expires_in: result.session.expiresInSeconds!, }, }, ctx, diff --git a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts index e0318464ed..e671a62d52 100644 --- a/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts +++ b/plugins/auth-backend/src/lib/legacy/adaptLegacyOAuthSignInResolver.ts @@ -39,7 +39,7 @@ export function adaptLegacyOAuthSignInResolver( scope: input.result.session.scope, id_token: input.result.session.idToken, token_type: input.result.session.tokenType, - expires_in: input.result.session.expiresInSeconds, + expires_in: input.result.session.expiresInSeconds!, }, }, }, diff --git a/plugins/auth-backend/src/providers/microsoft/provider.test.ts b/plugins/auth-backend/src/providers/microsoft/provider.test.ts index a1128ee6b9..7bbfd085d6 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.test.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.test.ts @@ -30,6 +30,10 @@ describe('MicrosoftAuthProvider', () => { const state = Buffer.from( `nonce=${encodeURIComponent(nonce)}&env=development`, ).toString('hex'); + const mockBackstageToken = `header.${Buffer.from( + JSON.stringify({ sub: 'user:default/mock' }), + 'utf8', + ).toString('base64')}.backstage`; const server = setupServer(); const microsoftApi = new FakeMicrosoftAPI(); @@ -64,10 +68,9 @@ describe('MicrosoftAuthProvider', () => { resolverContext: { issueToken: jest.fn(), findCatalogUser: jest.fn(), - signInWithCatalogUser: _ => - Promise.resolve({ - token: 'header.e30K.backstage', - }), + signInWithCatalogUser: async _ => ({ + token: mockBackstageToken, + }), } as AuthResolverContext, }) as AuthProviderRouteHandlers; @@ -209,8 +212,12 @@ describe('MicrosoftAuthProvider', () => { displayName: 'Conrad', }, backstageIdentity: { - token: 'header.e30K.backstage', - identity: { type: 'user', ownershipEntityRefs: [] }, + token: mockBackstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, }, }, }), @@ -330,8 +337,12 @@ describe('MicrosoftAuthProvider', () => { displayName: 'Conrad', }, backstageIdentity: { - token: 'header.e30K.backstage', - identity: { type: 'user', ownershipEntityRefs: [] }, + token: mockBackstageToken, + identity: { + type: 'user', + userEntityRef: 'user:default/mock', + ownershipEntityRefs: [], + }, }, }, }), @@ -430,7 +441,7 @@ describe('MicrosoftAuthProvider', () => { expect(response.json).toHaveBeenCalledWith( expect.objectContaining({ backstageIdentity: expect.objectContaining({ - token: 'header.e30K.backstage', + token: mockBackstageToken, }), }), ); diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 026a6263b7..e3bfc1c3f2 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -51,6 +51,7 @@ export interface RouterOptions { tokenManager: TokenManager; tokenFactoryAlgorithm?: string; providerFactories?: ProviderFactories; + disableDefaultProviderFactories?: boolean; catalogApi?: CatalogApi; } @@ -65,7 +66,7 @@ export async function createRouter( database, tokenManager, tokenFactoryAlgorithm, - providerFactories, + providerFactories = {}, catalogApi, } = options; const router = Router(); @@ -85,7 +86,9 @@ export async function createRouter( keyStore, keyDurationSeconds, logger: logger.child({ component: 'token-factory' }), - algorithm: tokenFactoryAlgorithm, + algorithm: + tokenFactoryAlgorithm ?? + config.getOptionalString('auth.identityTokenAlgorithm'), }); const secret = config.getOptionalString('auth.session.secret'); @@ -113,19 +116,21 @@ export async function createRouter( router.use(express.urlencoded({ extended: false })); router.use(express.json()); - const allProviderFactories = { - ...defaultAuthProviderFactories, - ...providerFactories, - }; - const providersConfig = config.getConfig('auth.providers'); - const configuredProviders = providersConfig.keys(); + const allProviderFactories = options.disableDefaultProviderFactories + ? providerFactories + : { + ...defaultAuthProviderFactories, + ...providerFactories, + }; + + const providersConfig = config.getOptionalConfig('auth.providers'); const isOriginAllowed = createOriginFilter(config); for (const [providerId, providerFactory] of Object.entries( allProviderFactories, )) { - if (configuredProviders.includes(providerId)) { + if (providersConfig?.has(providerId)) { logger.info(`Configuring auth provider: ${providerId}`); try { const provider = providerFactory({ diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md index a7fc9b1612..bc17c328d4 100644 --- a/plugins/auth-node/api-report.md +++ b/plugins/auth-node/api-report.md @@ -100,6 +100,7 @@ export type AuthResolverContext = { // @public export interface BackstageIdentityResponse extends BackstageSignInResult { + expiresInSeconds?: number; identity: BackstageUserIdentity; } @@ -377,7 +378,7 @@ export interface OAuthSession { // (undocumented) accessToken: string; // (undocumented) - expiresInSeconds: number; + expiresInSeconds?: number; // (undocumented) idToken?: string; // (undocumented) diff --git a/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.test.ts b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.test.ts index dac06f964e..d37224a11d 100644 --- a/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.test.ts +++ b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.test.ts @@ -23,7 +23,37 @@ function mkToken(payload: unknown) { } describe('prepareBackstageIdentityResponse', () => { + afterEach(jest.resetAllMocks); + it('parses a complete token to determine the identity', () => { + jest.spyOn(Date, 'now').mockReturnValue(5000); + + const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'], exp: 1005 }); + expect( + prepareBackstageIdentityResponse({ + token, + }), + ).toEqual({ + token, + expiresInSeconds: 1000, + identity: { + type: 'user', + userEntityRef: 'k:ns/n', + ownershipEntityRefs: ['k:ns/o'], + }, + }); + }); + + it('should reject tokens without subject', () => { + const token = mkToken({}); + expect(() => + prepareBackstageIdentityResponse({ + token, + }), + ).toThrow('Identity response must return a token with subject claim'); + }); + + it('should treat expiration as optional', () => { const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'] }); expect( prepareBackstageIdentityResponse({ @@ -38,4 +68,15 @@ describe('prepareBackstageIdentityResponse', () => { }, }); }); + + it('should reject tokens with negative expiration', () => { + jest.spyOn(Date, 'now').mockReturnValue(5000); + + const token = mkToken({ sub: 'k:ns/n', ent: ['k:ns/o'], exp: 1 }); + expect(() => + prepareBackstageIdentityResponse({ + token, + }), + ).toThrow('Identity response must not return an expired token'); + }); }); diff --git a/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.ts b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.ts index e2753d6648..33e9bafd0d 100644 --- a/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.ts +++ b/plugins/auth-node/src/identity/prepareBackstageIdentityResponse.ts @@ -39,14 +39,28 @@ export function prepareBackstageIdentityResponse( throw new InputError(`Identity response must return a token`); } - const { sub, ent } = parseJwtPayload(result.token); + const { sub, ent = [], exp: expStr } = parseJwtPayload(result.token); + if (!sub) { + throw new InputError( + `Identity response must return a token with subject claim`, + ); + } + + const expAt = Number(expStr); + + // Default to 1 hour if no expiration is set, in particular to make testing simpler + const exp = expAt ? Math.round(expAt - Date.now() / 1000) : undefined; + if (exp && exp < 0) { + throw new InputError(`Identity response must not return an expired token`); + } return { ...result, + expiresInSeconds: exp, identity: { type: 'user', userEntityRef: sub, - ownershipEntityRefs: ent ?? [], + ownershipEntityRefs: ent, }, }; } diff --git a/plugins/auth-node/src/oauth/types.ts b/plugins/auth-node/src/oauth/types.ts index c552de394e..bba961d27d 100644 --- a/plugins/auth-node/src/oauth/types.ts +++ b/plugins/auth-node/src/oauth/types.ts @@ -24,7 +24,7 @@ export interface OAuthSession { tokenType: string; idToken?: string; scope: string; - expiresInSeconds: number; + expiresInSeconds?: number; refreshToken?: string; } diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts index d52abf00fb..d4cb0ac48e 100644 --- a/plugins/auth-node/src/types.ts +++ b/plugins/auth-node/src/types.ts @@ -43,6 +43,11 @@ export interface BackstageSignInResult { * @public */ export interface BackstageIdentityResponse extends BackstageSignInResult { + /** + * The number of seconds until the token expires. If not set, it can be assumed that the token does not expire. + */ + expiresInSeconds?: number; + /** * A plaintext description of the identity that is encapsulated within the token. */ diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md index d083d00dba..2c916ec33b 100644 --- a/plugins/azure-devops/api-report.md +++ b/plugins/azure-devops/api-report.md @@ -14,8 +14,10 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { GitTag } from '@backstage/plugin-azure-devops-common'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { PullRequest } from '@backstage/plugin-azure-devops-common'; import { PullRequestOptions } from '@backstage/plugin-azure-devops-common'; +import { default as React_2 } from 'react'; import { Readme } from '@backstage/plugin-azure-devops-common'; import { ReadmeConfig } from '@backstage/plugin-azure-devops-common'; import { RepoBuild } from '@backstage/plugin-azure-devops-common'; @@ -163,14 +165,16 @@ export class AzureDevOpsClient implements AzureDevOpsApi { export const azureDevOpsPlugin: BackstagePlugin<{}, {}, {}>; // @public (undocumented) -export const AzurePullRequestsIcon: (props: SvgIconProps) => JSX.Element; +export const AzurePullRequestsIcon: ( + props: SvgIconProps, +) => React_2.JSX.Element; // @public (undocumented) export const AzurePullRequestsPage: (props: { projectName?: string | undefined; pollingInterval?: number | undefined; defaultColumnConfigs?: PullRequestColumnConfig[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export type BaseFilter = { @@ -223,22 +227,22 @@ export type CreatedByUserFilter = BaseFilter & ); // @public (undocumented) -export const EntityAzureGitTagsContent: () => JSX.Element; +export const EntityAzureGitTagsContent: () => JSX_2.Element; // @public (undocumented) export const EntityAzurePipelinesContent: (props: { defaultLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityAzurePullRequestsContent: (props: { defaultLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityAzureReadmeCard: (props: { maxHeight?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export type Filter = diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 96674d2cf2..f01ef6912d 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "humanize-duration": "^3.27.0", "luxon": "^3.0.0", "react-use": "^17.2.4" @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/azure-sites/api-report.md b/plugins/azure-sites/api-report.md index 38ca6a8fe2..bec89e17bc 100644 --- a/plugins/azure-sites/api-report.md +++ b/plugins/azure-sites/api-report.md @@ -13,6 +13,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -40,7 +42,7 @@ export class AzureSitesApiBackendClient implements AzureSitesApi { } // @public (undocumented) -export const AzureSitesOverviewWidget: () => JSX.Element; +export const AzureSitesOverviewWidget: () => React_2.JSX.Element; // @public (undocumented) export const azureSitesPlugin: BackstagePlugin< @@ -52,7 +54,7 @@ export const azureSitesPlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityAzureSitesOverviewWidget: () => JSX.Element; +export const EntityAzureSitesOverviewWidget: () => JSX_2.Element; // @public (undocumented) export const isAzureWebSiteNameAvailable: ( diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json index a4e50cccea..44cdc936a9 100644 --- a/plugins/azure-sites/package.json +++ b/plugins/azure-sites/package.json @@ -41,6 +41,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -59,7 +60,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/badges/api-report.md b/plugins/badges/api-report.md index b051bbb452..e73a887316 100644 --- a/plugins/badges/api-report.md +++ b/plugins/badges/api-report.md @@ -6,6 +6,7 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export const badgesPlugin: BackstagePlugin<{}, {}, {}>; @@ -14,5 +15,5 @@ export const badgesPlugin: BackstagePlugin<{}, {}, {}>; export const EntityBadgesDialog: (props: { open: boolean; onClose?: (() => any) | undefined; -}) => JSX.Element; +}) => JSX_2.Element; ``` diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 9146f64de5..ca40244cb7 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { diff --git a/plugins/bazaar/api-report.md b/plugins/bazaar/api-report.md index 11ce735bf5..282983b378 100644 --- a/plugins/bazaar/api-report.md +++ b/plugins/bazaar/api-report.md @@ -8,12 +8,14 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const BazaarOverviewCard: ( props: BazaarOverviewCardProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type BazaarOverviewCardProps = { @@ -24,7 +26,7 @@ export type BazaarOverviewCardProps = { }; // @public (undocumented) -export const BazaarPage: (props: BazaarPageProps) => JSX.Element; +export const BazaarPage: (props: BazaarPageProps) => JSX_2.Element; // @public (undocumented) export type BazaarPageProps = { @@ -44,7 +46,7 @@ export const bazaarPlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityBazaarInfoCard: () => JSX.Element | null; +export const EntityBazaarInfoCard: () => React_2.JSX.Element | null; // @public (undocumented) export const isBazaarAvailable: ( @@ -55,7 +57,7 @@ export const isBazaarAvailable: ( ) => Promise; // @public (undocumented) -export const SortView: (props: SortViewProps) => JSX.Element; +export const SortView: (props: SortViewProps) => React_2.JSX.Element; // @public (undocumented) export type SortViewProps = { diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 34d7490554..4a17558ca6 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -43,6 +43,7 @@ "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/pickers": "^3.3.10", "@testing-library/jest-dom": "^5.10.1", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "material-ui-search-bar": "^1.0.0", "react-hook-form": "^7.13.0", @@ -56,8 +57,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/dev-utils": "workspace:^", - "@testing-library/jest-dom": "^5.10.1", - "@types/react": "^16.13.1 || ^17.0.0" + "@testing-library/jest-dom": "^5.10.1" }, "files": [ "dist" diff --git a/plugins/bitrise/api-report.md b/plugins/bitrise/api-report.md index 01d10c61cf..7f2e44f04f 100644 --- a/plugins/bitrise/api-report.md +++ b/plugins/bitrise/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export const BITRISE_APP_ANNOTATION = 'bitrise.io/app'; @@ -15,7 +16,7 @@ export const BITRISE_APP_ANNOTATION = 'bitrise.io/app'; export const bitrisePlugin: BackstagePlugin<{}, {}, {}>; // @public (undocumented) -export const EntityBitriseContent: () => JSX.Element; +export const EntityBitriseContent: () => JSX_2.Element; // @public (undocumented) export const isBitriseAvailable: (entity: Entity) => boolean; diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 6d63cb6384..4cd8f9807d 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", "qs": "^6.9.6", @@ -58,7 +59,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "@types/recharts": "^1.8.15", "msw": "^1.0.0" }, diff --git a/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts b/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts index c9fea8aa28..ad1e94cad6 100644 --- a/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts +++ b/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts @@ -14,15 +14,7 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; @@ -40,12 +32,12 @@ describe('catalogModuleAwsS3EntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -66,16 +58,7 @@ describe('catalogModuleAwsS3EntityProvider', () => { features: [ catalogModuleAwsS3EntityProvider(), mockServices.rootConfig.factory({ data: config }), - createServiceFactory(() => ({ - service: coreServices.logger, - deps: {}, - factory: getVoidLogger, - }))(), - createServiceFactory(() => ({ - service: coreServices.scheduler, - deps: {}, - factory: () => scheduler, - }))(), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts index f26208353a..493f0dda51 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -18,11 +18,40 @@ import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { codeSearch, CodeSearchResponse } from './azure'; +import { + DefaultAzureDevOpsCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; describe('azure', () => { const server = setupServer(); setupRequestMockHandlers(server); + const createFixture = (host: string, token: string) => { + const azureConfig = { + host: host, + credentials: [ + { + personalAccessToken: token, + }, + ], + }; + const scmIntegrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [azureConfig], + }, + }), + ); + + return { + azureConfig: scmIntegrations.azure.byHost(host)?.config!, + credentialsProvider: + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(scmIntegrations), + }; + }; + describe('codeSearch', () => { it('returns empty when nothing is found', async () => { const response: CodeSearchResponse = { count: 0, results: [] }; @@ -34,6 +63,12 @@ describe('azure', () => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:* proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -42,9 +77,14 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', '', @@ -88,6 +128,12 @@ describe('azure', () => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:* proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -96,9 +142,14 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', '', @@ -132,6 +183,12 @@ describe('azure', () => { expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:backstage proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -140,9 +197,15 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); + await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', 'backstage', @@ -175,6 +238,12 @@ describe('azure', () => { expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); expect(req.body).toEqual({ searchText: 'path:/catalog-info.yaml repo:* proj:engineering', + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: 0, $top: 1000, }); @@ -183,9 +252,15 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'azuredevops.mycompany.com', + 'ABC', + ); + await expect( codeSearch( - { host: 'azuredevops.mycompany.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', '', @@ -236,9 +311,15 @@ describe('azure', () => { ), ); + const { credentialsProvider, azureConfig } = createFixture( + 'dev.azure.com', + 'ABC', + ); + await expect( codeSearch( - { host: 'dev.azure.com', token: 'ABC' }, + credentialsProvider, + azureConfig, 'shopify', 'engineering', 'backstage', diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.ts b/plugins/catalog-backend-module-azure/src/lib/azure.ts index 916d70a9fa..c8ff8dd739 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.ts @@ -16,8 +16,8 @@ import fetch from 'node-fetch'; import { + AzureDevOpsCredentialsProvider, AzureIntegrationConfig, - getAzureRequestOptions, } from '@backstage/integration'; export interface CodeSearchResponse { @@ -42,6 +42,7 @@ const PAGE_SIZE = 1000; // codeSearch returns all files that matches the given search path. export async function codeSearch( + credentialsProvider: AzureDevOpsCredentialsProvider, azureConfig: AzureIntegrationConfig, org: string, project: string, @@ -57,13 +58,24 @@ export async function codeSearch( let hasMorePages = true; do { + const credentials = await credentialsProvider.getCredentials({ + url: `https://${azureConfig.host}/${org}`, + }); + const response = await fetch(searchUrl, { - ...(await getAzureRequestOptions(azureConfig, { + headers: { + ...credentials?.headers, 'Content-Type': 'application/json', - })), + }, method: 'POST', body: JSON.stringify({ searchText: `path:${path} repo:${repo || '*'} proj:${project || '*'}`, + $orderBy: [ + { + field: 'path', + sortOrder: 'ASC', + }, + ], $skip: items.length, $top: PAGE_SIZE, }), diff --git a/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts b/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts index 3147a0871b..a20921fa3f 100644 --- a/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts +++ b/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; @@ -39,12 +32,12 @@ describe('catalogModuleAzureDevOpsEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -69,11 +62,7 @@ describe('catalogModuleAzureDevOpsEntityProvider', () => { catalogModuleAzureDevOpsEntityProvider(), mockServices.rootConfig.factory({ data: config }), mockServices.logger.factory(), - createServiceFactory(() => ({ - deps: {}, - service: coreServices.scheduler, - factory: async () => scheduler, - })), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts index 4a8e1d4537..36dd920621 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -158,6 +158,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', @@ -207,6 +208,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', @@ -248,6 +250,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', @@ -277,6 +280,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { await processor.readLocation(location, false, emitter); expect(mockCodeSearch).toHaveBeenCalledWith( + expect.anything(), { host: 'dev.azure.com' }, 'shopify', 'engineering', diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts index bad6067dc3..d9638e6cc7 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts @@ -16,6 +16,8 @@ import { Config } from '@backstage/config'; import { + AzureDevOpsCredentialsProvider, + DefaultAzureDevOpsCredentialsProvider, ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; @@ -46,6 +48,7 @@ import { codeSearch } from '../lib'; */ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; + private readonly credentialsProvider: AzureDevOpsCredentialsProvider; private readonly logger: Logger; static fromConfig(config: Config, options: { logger: Logger }) { @@ -63,6 +66,10 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { }) { this.integrations = options.integrations; this.logger = options.logger; + this.credentialsProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations( + options.integrations, + ); } getProcessorName(): string { @@ -93,6 +100,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { ); const files = await codeSearch( + this.credentialsProvider, azureConfig, org, project, diff --git a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts index 5abd23ce00..92398779c7 100644 --- a/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts +++ b/plugins/catalog-backend-module-azure/src/providers/AzureDevOpsEntityProvider.ts @@ -16,7 +16,12 @@ import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; import { Config } from '@backstage/config'; -import { AzureIntegration, ScmIntegrations } from '@backstage/integration'; +import { + AzureDevOpsCredentialsProvider, + AzureIntegration, + DefaultAzureDevOpsCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; import { EntityProvider, EntityProviderConnection, @@ -50,6 +55,9 @@ export class AzureDevOpsEntityProvider implements EntityProvider { }, ): AzureDevOpsEntityProvider[] { const providerConfigs = readAzureDevOpsConfigs(configRoot); + const scmIntegrations = ScmIntegrations.fromConfig(configRoot); + const credentialsProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(scmIntegrations); if (!options.schedule && !options.scheduler) { throw new Error('Either schedule or scheduler must be provided.'); @@ -79,6 +87,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { return new AzureDevOpsEntityProvider( providerConfig, integration, + credentialsProvider, options.logger, taskRunner, ); @@ -88,6 +97,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { private constructor( private readonly config: AzureDevOpsConfig, private readonly integration: AzureIntegration, + private readonly credentialsProvider: AzureDevOpsCredentialsProvider, logger: Logger, taskRunner: TaskRunner, ) { @@ -139,6 +149,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { logger.info('Discovering Azure DevOps catalog files'); const files = await codeSearch( + this.credentialsProvider, this.integration.config, this.config.organization, this.config.project, @@ -148,7 +159,10 @@ export class AzureDevOpsEntityProvider implements EntityProvider { logger.info(`Discovered ${files.length} catalog files`); - const locations = files.map(key => this.createLocationSpec(key)); + const targets = files.map(key => this.createObjectUrl(key)); + const locations = Array.from(new Set(targets)).map(key => + this.createLocationSpec(key), + ); await this.connection.applyMutation({ type: 'full', @@ -165,9 +179,7 @@ export class AzureDevOpsEntityProvider implements EntityProvider { ); } - private createLocationSpec(file: CodeSearchResultItem): LocationSpec { - const target = this.createObjectUrl(file); - + private createLocationSpec(target: string): LocationSpec { return { type: 'url', target: target, diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts index 83769fc3f5..c771a40acc 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { startTestBackend, mockServices } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; @@ -46,12 +39,12 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); await startTestBackend({ extensionPoints: [ @@ -75,11 +68,7 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { }, }, }), - createServiceFactory({ - service: coreServices.scheduler, - deps: {}, - factory: async () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts index ae4a636008..ab92e24fd7 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { catalogModuleBitbucketServerEntityProvider } from './catalogModuleBitbucketServerEntityProvider'; @@ -39,12 +32,12 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -73,11 +66,7 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { catalogModuleBitbucketServerEntityProvider(), mockServices.rootConfig.factory({ data: config }), mockServices.logger.factory(), - createServiceFactory({ - service: coreServices.scheduler, - deps: {}, - factory: async () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts b/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts index 9807814f61..4afcd8e2d4 100644 --- a/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; @@ -39,12 +32,12 @@ describe('catalogModuleGerritEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -79,11 +72,7 @@ describe('catalogModuleGerritEntityProvider', () => { catalogModuleGerritEntityProvider(), mockServices.rootConfig.factory({ data: config }), mockServices.logger.factory(), - createServiceFactory({ - service: coreServices.scheduler, - deps: {}, - factory: async () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts index 9e36966eec..171a641fc7 100644 --- a/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/module/catalogModuleGithubEntityProvider.test.ts @@ -14,19 +14,12 @@ * limitations under the License. */ -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; import { catalogModuleGithubEntityProvider } from './catalogModuleGithubEntityProvider'; import { GithubEntityProvider } from '../providers/GithubEntityProvider'; -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; describe('catalogModuleGithubEntityProvider', () => { it('should register provider at the catalog extension point', async () => { @@ -39,12 +32,12 @@ describe('catalogModuleGithubEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -65,11 +58,7 @@ describe('catalogModuleGithubEntityProvider', () => { features: [ catalogModuleGithubEntityProvider(), mockServices.rootConfig.factory({ data: config }), - createServiceFactory({ - service: coreServices.scheduler, - deps: {}, - factory: async () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts index 22759cafa0..46cfe403e8 100644 --- a/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; @@ -39,12 +32,12 @@ describe('catalogModuleGitlabDiscoveryEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { integrations: { @@ -78,11 +71,7 @@ describe('catalogModuleGitlabDiscoveryEntityProvider', () => { catalogModuleGitlabDiscoveryEntityProvider(), mockServices.rootConfig.factory({ data: config }), mockServices.logger.factory(), - createServiceFactory({ - deps: {}, - service: coreServices.scheduler, - factory: async () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts b/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts index ed0c776fa8..7d76ddf480 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/module/catalogModuleIncrementalIngestionEntityProvider.test.ts @@ -14,12 +14,8 @@ * limitations under the License. */ -import { - coreServices, - createBackendModule, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { startTestBackend } from '@backstage/backend-test-utils'; +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { IncrementalEntityProvider } from '../types'; import { @@ -40,18 +36,15 @@ describe('catalogModuleIncrementalIngestionEntityProvider', () => { }; const addEntityProvider = jest.fn(); - const httpRouterUse = jest.fn(); + + const httpRouterMock = mockServices.httpRouter.mock(); await startTestBackend({ extensionPoints: [ [catalogProcessingExtensionPoint, { addEntityProvider }], ], features: [ - createServiceFactory({ - service: coreServices.httpRouter, - deps: {}, - factory: () => ({ use: httpRouterUse }), - }), + httpRouterMock.factory, catalogModuleIncrementalIngestionEntityProvider(), createBackendModule({ pluginId: 'catalog', @@ -79,6 +72,6 @@ describe('catalogModuleIncrementalIngestionEntityProvider', () => { expect(addEntityProvider.mock.calls[0][0].getProviderName()).toBe( 'provider1', ); - expect(httpRouterUse).toHaveBeenCalledTimes(1); + expect(httpRouterMock.use).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts index 96fec41cbe..d36995b1c8 100644 --- a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { Duration } from 'luxon'; @@ -39,12 +32,12 @@ describe('catalogModuleMicrosoftGraphOrgEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -70,11 +63,7 @@ describe('catalogModuleMicrosoftGraphOrgEntityProvider', () => { features: [ catalogModuleMicrosoftGraphOrgEntityProvider(), mockServices.rootConfig.factory({ data: config }), - createServiceFactory(() => ({ - deps: {}, - service: coreServices.scheduler, - factory: async () => scheduler, - })), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts index 3ea54d8c8f..f931b2c741 100644 --- a/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/module/catalogModulePuppetDbEntityProvider.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { - PluginTaskScheduler, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { catalogModulePuppetDbEntityProvider } from './catalogModulePuppetDbEntityProvider'; @@ -38,12 +31,12 @@ describe('catalogModulePuppetDbEntityProvider', () => { }, }; const runner = jest.fn(); - const scheduler = { - createScheduledTaskRunner: (schedule: TaskScheduleDefinition) => { + const scheduler = mockServices.scheduler.mock({ + createScheduledTaskRunner(schedule) { usedSchedule = schedule; - return runner; + return { run: runner }; }, - } as unknown as PluginTaskScheduler; + }); const config = { catalog: { @@ -64,11 +57,7 @@ describe('catalogModulePuppetDbEntityProvider', () => { features: [ catalogModulePuppetDbEntityProvider(), mockServices.rootConfig.factory({ data: config }), - createServiceFactory(() => ({ - deps: {}, - service: coreServices.scheduler, - factory: async () => scheduler, - })), + scheduler.factory, ], }); diff --git a/plugins/catalog-backend/README.md b/plugins/catalog-backend/README.md index 1e2bce3142..cd921499d3 100644 --- a/plugins/catalog-backend/README.md +++ b/plugins/catalog-backend/README.md @@ -86,5 +86,5 @@ some example entities. ## Links -- [catalog](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend) +- [catalog](https://github.com/backstage/backstage/tree/master/plugins/catalog) is the frontend interface for this plugin. diff --git a/plugins/catalog-backend/src/schema/openapi.generated.ts b/plugins/catalog-backend/src/schema/openapi.generated.ts index 7428e5ace2..77f33a2ca5 100644 --- a/plugins/catalog-backend/src/schema/openapi.generated.ts +++ b/plugins/catalog-backend/src/schema/openapi.generated.ts @@ -1087,7 +1087,10 @@ export const spec = { name: 'facet', required: true, schema: { - type: 'string', + type: 'array', + items: { + type: 'string', + }, }, }, { diff --git a/plugins/catalog-backend/src/schema/openapi.yaml b/plugins/catalog-backend/src/schema/openapi.yaml index 34e21adffd..79049955d9 100644 --- a/plugins/catalog-backend/src/schema/openapi.yaml +++ b/plugins/catalog-backend/src/schema/openapi.yaml @@ -819,7 +819,9 @@ paths: name: facet required: true schema: - type: string + type: array + items: + type: string - $ref: '#/components/parameters/filter' /locations: post: diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index 80578db6ed..ce56245c49 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -11,8 +11,10 @@ import { DependencyGraphTypes } from '@backstage/core-components'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; import { JsonObject } from '@backstage/types'; +import { JSX as JSX_2 } from 'react'; import { MouseEvent as MouseEvent_2 } from 'react'; import { MouseEventHandler } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -35,7 +37,7 @@ export const CatalogGraphPage: ( } | undefined; } & Partial, -) => JSX.Element; +) => JSX_2.Element; // @public export const catalogGraphPlugin: BackstagePlugin< @@ -73,7 +75,7 @@ export const EntityCatalogGraphCard: ( height?: number | undefined; title?: string | undefined; }, -) => JSX.Element; +) => JSX_2.Element; // @public export type EntityEdge = DependencyGraphTypes.DependencyEdge; @@ -102,7 +104,7 @@ export type EntityNodeData = { // @public export const EntityRelationsGraph: ( props: EntityRelationsGraphProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type EntityRelationsGraphProps = { diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md index b4c8e854aa..f507967d37 100644 --- a/plugins/catalog-import/api-report.md +++ b/plugins/catalog-import/api-report.md @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { FieldErrors } from 'react-hook-form'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmAuthApi } from '@backstage/integration-react'; @@ -46,7 +47,7 @@ export type AnalyzeResult = // @public export const AutocompleteTextField: ( props: AutocompleteTextFieldProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface AutocompleteTextFieldProps { @@ -126,7 +127,7 @@ export class CatalogImportClient implements CatalogImportApi { } // @public -export const CatalogImportPage: () => JSX.Element; +export const CatalogImportPage: () => JSX_2.Element; // @public const catalogImportPlugin: BackstagePlugin< @@ -148,12 +149,12 @@ export function defaultGenerateStepper( ): StepperProvider; // @public -export const DefaultImportPage: () => JSX.Element; +export const DefaultImportPage: () => React_2.JSX.Element; // @public export const EntityListComponent: ( props: EntityListComponentProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface EntityListComponentProps { @@ -182,7 +183,9 @@ export type ImportFlows = | 'no-location'; // @public -export const ImportInfoCard: (props: ImportInfoCardProps) => JSX.Element; +export const ImportInfoCard: ( + props: ImportInfoCardProps, +) => React_2.JSX.Element; // @public export interface ImportInfoCardProps { @@ -205,7 +208,7 @@ export type ImportState = State & { }; // @public -export const ImportStepper: (props: ImportStepperProps) => JSX.Element; +export const ImportStepper: (props: ImportStepperProps) => React_2.JSX.Element; // @public export interface ImportStepperProps { @@ -223,7 +226,7 @@ export interface ImportStepperProps { // @public export const PreparePullRequestForm: >( props: PreparePullRequestFormProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type PreparePullRequestFormProps< @@ -266,7 +269,7 @@ export type PrepareResult = // @public export const PreviewCatalogInfoComponent: ( props: PreviewCatalogInfoComponentProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface PreviewCatalogInfoComponentProps { @@ -284,7 +287,7 @@ export interface PreviewCatalogInfoComponentProps { // @public export const PreviewPullRequestComponent: ( props: PreviewPullRequestComponentProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface PreviewPullRequestComponentProps { @@ -302,7 +305,7 @@ export interface PreviewPullRequestComponentProps { // @public export const StepInitAnalyzeUrl: ( props: StepInitAnalyzeUrlProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface StepInitAnalyzeUrlProps { @@ -326,7 +329,7 @@ export interface StepInitAnalyzeUrlProps { // @public export const StepPrepareCreatePullRequest: ( props: StepPrepareCreatePullRequestProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface StepPrepareCreatePullRequestProps { diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index b5ab8b6c7f..0891875e3c 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { ApiRef } from '@backstage/core-plugin-api'; import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; @@ -30,7 +28,7 @@ import { TableOptions } from '@backstage/core-components'; // @public export const AsyncEntityProvider: ( props: AsyncEntityProviderProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export interface AsyncEntityProviderProps { @@ -62,15 +60,15 @@ export const catalogApiRef: ApiRef; // @public (undocumented) export const CatalogFilterLayout: { - (props: { children: React_2.ReactNode }): JSX.Element; + (props: { children: React_2.ReactNode }): React_2.JSX.Element; Filters: (props: { children: React_2.ReactNode; options?: { drawerBreakpoint?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number; drawerAnchor?: 'left' | 'right' | 'top' | 'bottom'; }; - }) => JSX.Element; - Content: (props: { children: React_2.ReactNode }) => JSX.Element; + }) => React_2.JSX.Element; + Content: (props: { children: React_2.ReactNode }) => React_2.JSX.Element; }; // @public (undocumented) @@ -177,7 +175,7 @@ export class EntityKindFilter implements EntityFilter { // @public (undocumented) export const EntityKindPicker: ( props: EntityKindPickerProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public export interface EntityKindPickerProps { @@ -202,7 +200,7 @@ export class EntityLifecycleFilter implements EntityFilter { // @public (undocumented) export const EntityLifecyclePicker: (props: { initialFilter?: string[]; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export const EntityListContext: React_2.Context< @@ -229,7 +227,7 @@ export type EntityListContextProps< // @public export const EntityListProvider: ( props: PropsWithChildren<{}>, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type EntityLoadingStatus = { @@ -251,7 +249,7 @@ export class EntityNamespaceFilter implements EntityFilter { } // @public (undocumented) -export const EntityNamespacePicker: () => JSX.Element; +export const EntityNamespacePicker: () => React_2.JSX.Element; // @public export class EntityOrphanFilter implements EntityFilter { @@ -275,7 +273,7 @@ export class EntityOwnerFilter implements EntityFilter { // @public (undocumented) export const EntityOwnerPicker: ( props?: EntityOwnerPickerProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public (undocumented) export type EntityOwnerPickerProps = { @@ -285,7 +283,7 @@ export type EntityOwnerPickerProps = { // @public export const EntityPeekAheadPopover: ( props: EntityPeekAheadPopoverProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type EntityPeekAheadPopoverProps = PropsWithChildren<{ @@ -294,10 +292,12 @@ export type EntityPeekAheadPopoverProps = PropsWithChildren<{ }>; // @public (undocumented) -export const EntityProcessingStatusPicker: () => JSX.Element; +export const EntityProcessingStatusPicker: () => React_2.JSX.Element; // @public -export const EntityProvider: (props: EntityProviderProps) => JSX.Element; +export const EntityProvider: ( + props: EntityProviderProps, +) => React_2.JSX.Element; // @public export interface EntityProviderProps { @@ -321,7 +321,7 @@ export type EntityRefLinkProps = { // @public export function EntityRefLinks< TRef extends string | CompoundEntityRef | Entity, ->(props: EntityRefLinksProps): JSX.Element; +>(props: EntityRefLinksProps): React_2.JSX.Element; // @public export type EntityRefLinksProps< @@ -357,7 +357,7 @@ export const entityRouteRef: RouteRef<{ }>; // @public -export const EntitySearchBar: () => JSX.Element; +export const EntitySearchBar: () => React_2.JSX.Element; // @public (undocumented) export type EntitySourceLocation = { @@ -367,7 +367,7 @@ export type EntitySourceLocation = { // @public export const EntityTable: { - (props: EntityTableProps): JSX.Element; + (props: EntityTableProps): React_2.JSX.Element; columns: Readonly<{ createEntityRefColumn(options: { defaultKind?: string | undefined; @@ -421,7 +421,9 @@ export class EntityTagFilter implements EntityFilter { } // @public (undocumented) -export const EntityTagPicker: (props: EntityTagPickerProps) => JSX.Element; +export const EntityTagPicker: ( + props: EntityTagPickerProps, +) => React_2.JSX.Element; // @public (undocumented) export type EntityTagPickerProps = { @@ -453,7 +455,7 @@ export class EntityTypeFilter implements EntityFilter { // @public (undocumented) export const EntityTypePicker: ( props: EntityTypePickerProps, -) => JSX.Element | null; +) => React_2.JSX.Element | null; // @public export interface EntityTypePickerProps { @@ -464,7 +466,9 @@ export interface EntityTypePickerProps { } // @public -export const FavoriteEntity: (props: FavoriteEntityProps) => JSX.Element; +export const FavoriteEntity: ( + props: FavoriteEntityProps, +) => React_2.JSX.Element; // @public (undocumented) export type FavoriteEntityProps = ComponentProps & { @@ -500,7 +504,7 @@ export function InspectEntityDialog(props: { open: boolean; entity: Entity; onClose: () => void; -}): JSX.Element | null; +}): React_2.JSX.Element | null; // @public (undocumented) export function MockEntityListContextProvider< @@ -509,7 +513,7 @@ export function MockEntityListContextProvider< props: PropsWithChildren<{ value?: Partial>; }>, -): JSX.Element; +): React_2.JSX.Element; // @public export class MockStarredEntitiesApi implements StarredEntitiesApi { @@ -531,7 +535,7 @@ export const starredEntitiesApiRef: ApiRef; // @public (undocumented) export const UnregisterEntityDialog: ( props: UnregisterEntityDialogProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type UnregisterEntityDialogProps = { @@ -607,7 +611,9 @@ export class UserListFilter implements EntityFilter { export type UserListFilterKind = 'owned' | 'starred' | 'all'; // @public (undocumented) -export const UserListPicker: (props: UserListPickerProps) => JSX.Element; +export const UserListPicker: ( + props: UserListPickerProps, +) => React_2.JSX.Element; // @public (undocumented) export type UserListPickerProps = { diff --git a/plugins/catalog-unprocessed-entities/api-report.md b/plugins/catalog-unprocessed-entities/api-report.md index 0ccf9716f2..3ded42785a 100644 --- a/plugins/catalog-unprocessed-entities/api-report.md +++ b/plugins/catalog-unprocessed-entities/api-report.md @@ -8,6 +8,8 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -25,7 +27,7 @@ export type CatalogUnprocessedEntitiesApiResponse = { }; // @public -export const CatalogUnprocessedEntitiesPage: () => JSX.Element; +export const CatalogUnprocessedEntitiesPage: () => JSX_2.Element; // @public export const catalogUnprocessedEntitiesPlugin: BackstagePlugin< @@ -37,7 +39,7 @@ export const catalogUnprocessedEntitiesPlugin: BackstagePlugin< >; // @public (undocumented) -export const UnprocessedEntitiesContent: () => JSX.Element; +export const UnprocessedEntitiesContent: () => React_2.JSX.Element; // @public export type UnprocessedEntity = { diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index 2a0d191b62..52283e0860 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -36,6 +36,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.60", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 3e33b16d8b..c8956dcbe8 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -15,6 +15,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { IndexableDocument } from '@backstage/plugin-search-common'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { Observable } from '@backstage/types'; import { Overrides } from '@material-ui/core/styles/overrides'; import { default as React_2 } from 'react'; @@ -39,7 +40,7 @@ export interface AboutCardProps { } // @public (undocumented) -export function AboutContent(props: AboutContentProps): JSX.Element; +export function AboutContent(props: AboutContentProps): React_2.JSX.Element; // @public export interface AboutContentProps { @@ -48,7 +49,7 @@ export interface AboutContentProps { } // @public (undocumented) -export function AboutField(props: AboutFieldProps): JSX.Element; +export function AboutField(props: AboutFieldProps): React_2.JSX.Element; // @public export interface AboutFieldProps { @@ -79,7 +80,9 @@ export const CatalogEntityPage: () => JSX.Element; export const CatalogIndexPage: (props: DefaultCatalogPageProps) => JSX.Element; // @public @deprecated (undocumented) -export function CatalogKindHeader(props: CatalogKindHeaderProps): JSX.Element; +export function CatalogKindHeader( + props: CatalogKindHeaderProps, +): React_2.JSX.Element; // @public export interface CatalogKindHeaderProps { @@ -137,7 +140,7 @@ export interface CatalogSearchResultListItemProps { // @public (undocumented) export const CatalogTable: { - (props: CatalogTableProps): JSX.Element; + (props: CatalogTableProps): React_2.JSX.Element; columns: Readonly<{ createNameColumn( options?: @@ -303,7 +306,7 @@ export const EntityHasSubcomponentsCard: ( export const EntityHasSystemsCard: (props: HasSystemsCardProps) => JSX.Element; // @public (undocumented) -export const EntityLabelsCard: (props: EntityLabelsCardProps) => JSX.Element; +export const EntityLabelsCard: (props: EntityLabelsCardProps) => JSX_2.Element; // @public (undocumented) export interface EntityLabelsCardProps { @@ -315,7 +318,7 @@ export interface EntityLabelsCardProps { // @public export const EntityLayout: { - (props: EntityLayoutProps): JSX.Element; + (props: EntityLayoutProps): React_2.JSX.Element; Route: (props: EntityLayoutRouteProps) => null; }; @@ -350,7 +353,7 @@ export type EntityLayoutRouteProps = { }; // @public (undocumented) -export const EntityLinksCard: (props: EntityLinksCardProps) => JSX.Element; +export const EntityLinksCard: (props: EntityLinksCardProps) => JSX_2.Element; // @public (undocumented) export interface EntityLinksCardProps { @@ -366,10 +369,10 @@ export type EntityLinksEmptyStateClassKey = 'code'; // @public @deprecated (undocumented) export const EntityListContainer: (props: { children: ReactNode; -}) => JSX.Element; +}) => JSX_2.Element; // @public -export function EntityOrphanWarning(): JSX.Element; +export function EntityOrphanWarning(): React_2.JSX.Element; // @public (undocumented) export interface EntityPredicates { @@ -380,14 +383,14 @@ export interface EntityPredicates { } // @public -export function EntityProcessingErrorsPanel(): JSX.Element | null; +export function EntityProcessingErrorsPanel(): React_2.JSX.Element | null; // @public -export function EntityRelationWarning(): JSX.Element | null; +export function EntityRelationWarning(): React_2.JSX.Element | null; // @public (undocumented) export const EntitySwitch: { - (props: EntitySwitchProps): JSX.Element; + (props: EntitySwitchProps): React_2.JSX.Element; Case: (_props: EntitySwitchCaseProps) => null; }; @@ -428,7 +431,7 @@ export const FilterContainer: (props: { drawerAnchor?: 'left' | 'top' | 'bottom' | 'right' | undefined; } | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public @deprecated (undocumented) export const FilteredEntityLayout: (props: { @@ -445,6 +448,8 @@ export function hasCatalogProcessingErrors( // @public (undocumented) export interface HasComponentsCardProps { + // (undocumented) + title?: string; // (undocumented) variant?: InfoCardVariants; } @@ -462,6 +467,8 @@ export function hasRelationWarnings( // @public (undocumented) export interface HasResourcesCardProps { + // (undocumented) + title?: string; // (undocumented) variant?: InfoCardVariants; } @@ -471,11 +478,15 @@ export interface HasSubcomponentsCardProps { // (undocumented) tableOptions?: TableOptions; // (undocumented) + title?: string; + // (undocumented) variant?: InfoCardVariants; } // @public (undocumented) export interface HasSystemsCardProps { + // (undocumented) + title?: string; // (undocumented) variant?: InfoCardVariants; } diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx index c93922e938..d9998b0ca5 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx @@ -59,6 +59,50 @@ describe('EntitySwitch', () => { expect(screen.queryByText('C')).not.toBeInTheDocument(); }); + it('should render the default case if entity is not found', () => { + const content = ( + + + + + + ); + + render( + + + {content} + + , + ); + + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); + expect(screen.queryByText('C')).toBeInTheDocument(); + }); + + it(`shouldn't render any children if entity is loading and no entity exists in the context`, () => { + const content = ( + + + + + + ); + + render( + + + {content} + + , + ); + + expect(screen.queryByText('A')).not.toBeInTheDocument(); + expect(screen.queryByText('B')).not.toBeInTheDocument(); + expect(screen.queryByText('C')).not.toBeInTheDocument(); + }); + it('should render the fallback if no cases are matching', () => { const content = ( @@ -170,7 +214,7 @@ describe('EntitySwitch', () => { expect(screen.queryByText('A')).not.toBeInTheDocument(); expect(screen.queryByText('B')).not.toBeInTheDocument(); - expect(screen.queryByText('C')).not.toBeInTheDocument(); + expect(screen.queryByText('C')).toBeInTheDocument(); }); it('should switch child when filters switch', () => { diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index b12495052e..9eab743308 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -64,7 +64,7 @@ export interface EntitySwitchProps { /** @public */ export const EntitySwitch = (props: EntitySwitchProps) => { - const { entity } = useAsyncEntity(); + const { entity, loading } = useAsyncEntity(); const apis = useApiHolder(); const results = useElementFilter( @@ -77,14 +77,21 @@ export const EntitySwitch = (props: EntitySwitchProps) => { }) .getElements() .flatMap((element: ReactElement) => { - // If the entity is missing or there is an error, render nothing - if (!entity) { + if (loading && !entity) { return []; } const { if: condition, children: elementsChildren } = element.props as EntitySwitchCase; + if (!entity) { + return [ + { + if: condition === undefined, + children: elementsChildren, + }, + ]; + } return [ { if: condition?.(entity, { apis }), @@ -92,7 +99,7 @@ export const EntitySwitch = (props: EntitySwitchProps) => { }, ]; }), - [apis, entity], + [apis, entity, loading], ); const hasAsyncCases = results.some( diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx index 9654152a5c..9071dac030 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -27,14 +27,15 @@ import { /** @public */ export interface HasComponentsCardProps { variant?: InfoCardVariants; + title?: string; } export function HasComponentsCard(props: HasComponentsCardProps) { - const { variant = 'gridItem' } = props; + const { variant = 'gridItem', title = 'Has components' } = props; return ( ; // @public (undocumented) -export const EntityCircleCIContent: () => JSX.Element; +export const EntityCircleCIContent: () => JSX_2.Element; export { GitType }; @@ -79,5 +81,5 @@ export { isCircleCIAvailable }; export { isCircleCIAvailable as isPluginApplicableToEntity }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 5576faf2ce..a001f22758 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -41,6 +41,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "circleci-api": "^4.0.0", "humanize-duration": "^3.27.0", "lodash": "^4.17.21", @@ -63,7 +64,6 @@ "@testing-library/user-event": "^14.0.0", "@types/humanize-duration": "^3.25.1", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/cloudbuild/api-report.md b/plugins/cloudbuild/api-report.md index deacf96acd..dc2aacbc18 100644 --- a/plugins/cloudbuild/api-report.md +++ b/plugins/cloudbuild/api-report.md @@ -8,7 +8,9 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -109,17 +111,17 @@ export { cloudbuildPlugin }; export { cloudbuildPlugin as plugin }; // @public (undocumented) -export const EntityCloudbuildContent: () => JSX.Element; +export const EntityCloudbuildContent: () => JSX_2.Element; // @public (undocumented) export const EntityLatestCloudbuildRunCard: (props: { branch: string; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityLatestCloudbuildsForBranchCard: (props: { branch: string; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export interface FETCHSOURCE { @@ -135,12 +137,14 @@ export { isCloudbuildAvailable }; export { isCloudbuildAvailable as isPluginApplicableToEntity }; // @public (undocumented) -export const LatestWorkflowRunCard: (props: { branch: string }) => JSX.Element; +export const LatestWorkflowRunCard: (props: { + branch: string; +}) => React_2.JSX.Element; // @public (undocumented) export const LatestWorkflowsForBranchCard: (props: { branch: string; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export interface Options { @@ -181,7 +185,7 @@ export interface Results { } // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // @public (undocumented) export interface Source { diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index cad764c8b9..bf1c8815d5 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -41,6 +41,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "qs": "^6.9.4", "react-use": "^17.2.4" @@ -60,7 +61,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/code-climate/api-report.md b/plugins/code-climate/api-report.md index f4a2d3a26d..32359f663f 100644 --- a/plugins/code-climate/api-report.md +++ b/plugins/code-climate/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -46,7 +47,7 @@ export const codeClimatePlugin: BackstagePlugin< >; // @public (undocumented) -export const EntityCodeClimateCard: () => JSX.Element; +export const EntityCodeClimateCard: () => JSX_2.Element; // @public (undocumented) export class MockCodeClimateApi implements CodeClimateApi { diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json index 1bb57e7f09..f1f02fa508 100644 --- a/plugins/code-climate/package.json +++ b/plugins/code-climate/package.json @@ -36,6 +36,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "humanize-duration": "^3.27.1", "luxon": "^3.0.0", "react-use": "^17.2.4" @@ -56,7 +57,6 @@ "@types/humanize-duration": "^3.27.1", "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/code-coverage/src/api.ts b/plugins/code-coverage/src/api.ts index 081f1c7999..942e5a8a47 100644 --- a/plugins/code-coverage/src/api.ts +++ b/plugins/code-coverage/src/api.ts @@ -20,10 +20,13 @@ import { } from '@backstage/catalog-model'; import { ResponseError } from '@backstage/errors'; import { JsonCodeCoverage, JsonCoverageHistory } from './types'; -import { createApiRef, DiscoveryApi } from '@backstage/core-plugin-api'; +import { + createApiRef, + DiscoveryApi, + FetchApi, +} from '@backstage/core-plugin-api'; export type CodeCoverageApi = { - discovery: DiscoveryApi; getCoverageForEntity: ( entity: CompoundEntityRef, ) => Promise; @@ -42,18 +45,22 @@ export const codeCoverageApiRef = createApiRef({ }); export class CodeCoverageRestApi implements CodeCoverageApi { - url: string = ''; + private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; - constructor(public discovery: DiscoveryApi) {} + public constructor(options: { + discoveryApi: DiscoveryApi; + fetchApi: FetchApi; + }) { + this.discoveryApi = options.discoveryApi; + this.fetchApi = options.fetchApi; + } private async fetch( path: string, - init?: RequestInit, ): Promise { - if (!this.url) { - this.url = await this.discovery.getBaseUrl('code-coverage'); - } - const resp = await fetch(`${this.url}${path}`, init); + const url = await this.discoveryApi.getBaseUrl('code-coverage'); + const resp = await this.fetchApi.fetch(`${url}${path}`); if (!resp.ok) { throw await ResponseError.fromResponse(resp); } diff --git a/plugins/code-coverage/src/plugin.ts b/plugins/code-coverage/src/plugin.ts index f137e9d488..391595f142 100644 --- a/plugins/code-coverage/src/plugin.ts +++ b/plugins/code-coverage/src/plugin.ts @@ -21,6 +21,7 @@ import { createPlugin, createRoutableExtension, discoveryApiRef, + fetchApiRef, } from '@backstage/core-plugin-api'; /** @@ -34,8 +35,9 @@ export const codeCoveragePlugin = createPlugin({ apis: [ createApiFactory({ api: codeCoverageApiRef, - deps: { discoveryApi: discoveryApiRef }, - factory: ({ discoveryApi }) => new CodeCoverageRestApi(discoveryApi), + deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef }, + factory: ({ discoveryApi, fetchApi }) => + new CodeCoverageRestApi({ discoveryApi, fetchApi }), }), ], }); diff --git a/plugins/codescene/api-report.md b/plugins/codescene/api-report.md index 75dfddd09e..662968d3c9 100644 --- a/plugins/codescene/api-report.md +++ b/plugins/codescene/api-report.md @@ -7,13 +7,14 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const CodeSceneIcon: IconComponent; // @public (undocumented) -export const CodeScenePage: () => JSX.Element; +export const CodeScenePage: () => JSX_2.Element; // @public (undocumented) export const codescenePlugin: BackstagePlugin< @@ -28,7 +29,7 @@ export const codescenePlugin: BackstagePlugin< >; // @public (undocumented) -export const CodeSceneProjectDetailsPage: () => JSX.Element; +export const CodeSceneProjectDetailsPage: () => JSX_2.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json index 4c5be3949d..7c355455ae 100644 --- a/plugins/codescene/package.json +++ b/plugins/codescene/package.json @@ -36,6 +36,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "rc-progress": "3.4.2", "react-use": "^17.2.4" }, @@ -54,7 +55,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/config-schema/api-report.md b/plugins/config-schema/api-report.md index 7d110c34d4..95d352d1b7 100644 --- a/plugins/config-schema/api-report.md +++ b/plugins/config-schema/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { Observable } from '@backstage/types'; import { RouteRef } from '@backstage/core-plugin-api'; import { Schema } from 'jsonschema'; @@ -21,7 +22,7 @@ export interface ConfigSchemaApi { export const configSchemaApiRef: ApiRef; // @public (undocumented) -export const ConfigSchemaPage: () => JSX.Element; +export const ConfigSchemaPage: () => JSX_2.Element; // @public (undocumented) export const configSchemaPlugin: BackstagePlugin< diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index c3ffc236e6..b090be546d 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "jsonschema": "^1.2.6", "react-use": "^17.2.4", "zen-observable": "^0.10.0" @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/cost-insights/api-report.md b/plugins/cost-insights/api-report.md index 00cbc4c3af..b37804c93d 100644 --- a/plugins/cost-insights/api-report.md +++ b/plugins/cost-insights/api-report.md @@ -16,6 +16,7 @@ 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 { JSX as JSX_2 } from 'react'; 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'; @@ -23,6 +24,7 @@ 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 { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RefAttributes } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -140,7 +142,7 @@ export enum AlertStatus { } // @public (undocumented) -export const BarChart: (props: BarChartProps) => JSX.Element; +export const BarChart: (props: BarChartProps) => React_2.JSX.Element; // @public @deprecated (undocumented) export interface BarChartData extends BarChartOptions {} @@ -148,7 +150,7 @@ export interface BarChartData extends BarChartOptions {} // @public (undocumented) export const BarChartLegend: ( props: PropsWithChildren, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type BarChartLegendOptions = { @@ -192,12 +194,12 @@ export type BarChartProps = { // @public (undocumented) export const BarChartTooltip: ( props: PropsWithChildren, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export const BarChartTooltipItem: ( props: BarChartTooltipItemProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type BarChartTooltipItemProps = { @@ -247,12 +249,12 @@ export type ConfigContextProps = { export type Cost = common.Cost; // @public (undocumented) -export const CostGrowth: (props: CostGrowthProps) => JSX.Element; +export const CostGrowth: (props: CostGrowthProps) => React_2.JSX.Element; // @public (undocumented) export const CostGrowthIndicator: ( props: CostGrowthIndicatorProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type CostGrowthIndicatorProps = TypographyProps & { @@ -291,10 +293,10 @@ export type CostInsightsApi = { export const costInsightsApiRef: ApiRef; // @public (undocumented) -export const CostInsightsLabelDataflowInstructionsPage: () => JSX.Element; +export const CostInsightsLabelDataflowInstructionsPage: () => JSX_2.Element; // @public (undocumented) -export const CostInsightsPage: () => JSX.Element; +export const CostInsightsPage: () => JSX_2.Element; // @public (undocumented) export type CostInsightsPalette = BackstagePalette & @@ -331,7 +333,7 @@ export { costInsightsPlugin }; export { costInsightsPlugin as plugin }; // @public (undocumented) -export const CostInsightsProjectGrowthInstructionsPage: () => JSX.Element; +export const CostInsightsProjectGrowthInstructionsPage: () => JSX_2.Element; // @public (undocumented) export interface CostInsightsTheme extends BackstageTheme { @@ -418,7 +420,7 @@ export const EngineerThreshold = 0.5; export type Entity = common.Entity; // @public -export const EntityCostInsightsContent: () => JSX.Element; +export const EntityCostInsightsContent: () => JSX_2.Element; // @public (undocumented) export class ExampleCostInsightsClient implements CostInsightsApi { @@ -483,7 +485,7 @@ export enum IconType { // @public (undocumented) export const LegendItem: ( props: PropsWithChildren, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type LegendItemProps = { @@ -507,7 +509,7 @@ export type MetricData = common.MetricData; // @public (undocumented) export const MockConfigProvider: ( props: MockConfigProviderProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type MockConfigProviderProps = PropsWithChildren< @@ -517,7 +519,7 @@ export type MockConfigProviderProps = PropsWithChildren< // @public (undocumented) export const MockCurrencyProvider: ( props: MockCurrencyProviderProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export type MockCurrencyProviderProps = PropsWithChildren< @@ -567,7 +569,7 @@ export class ProjectGrowthAlert implements Alert { // (undocumented) data: ProjectGrowthData; // (undocumented) - get element(): JSX.Element; + get element(): React_2.JSX.Element; // (undocumented) get subtitle(): string; // (undocumented) @@ -623,7 +625,7 @@ export class UnlabeledDataflowAlert implements Alert { // (undocumented) data: UnlabeledDataflowData; // (undocumented) - get element(): JSX.Element; + get element(): React_2.JSX.Element; // (undocumented) status?: AlertStatus; // (undocumented) diff --git a/plugins/devtools/api-report.md b/plugins/devtools/api-report.md index 3e3fb99ff7..f0a9280162 100644 --- a/plugins/devtools/api-report.md +++ b/plugins/devtools/api-report.md @@ -7,15 +7,17 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { default as default_2 } from 'react'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core'; // @public (undocumented) -export const ConfigContent: () => JSX.Element; +export const ConfigContent: () => React_2.JSX.Element; // @public export const DevToolsLayout: { - ({ children, title, subtitle }: DevToolsLayoutProps): JSX.Element; + ({ children, title, subtitle }: DevToolsLayoutProps): default_2.JSX.Element; Route: (props: SubRoute) => null; }; @@ -27,7 +29,7 @@ export type DevToolsLayoutProps = { }; // @public (undocumented) -export const DevToolsPage: () => JSX.Element; +export const DevToolsPage: () => JSX_2.Element; // @public (undocumented) export const devToolsPlugin: BackstagePlugin< @@ -39,10 +41,10 @@ export const devToolsPlugin: BackstagePlugin< >; // @public (undocumented) -export const ExternalDependenciesContent: () => JSX.Element; +export const ExternalDependenciesContent: () => React_2.JSX.Element; // @public (undocumented) -export const InfoContent: () => JSX.Element; +export const InfoContent: () => React_2.JSX.Element; // @public (undocumented) export type SubRoute = { diff --git a/plugins/dynatrace/api-report.md b/plugins/dynatrace/api-report.md index 1dded736ad..0f25fc3108 100644 --- a/plugins/dynatrace/api-report.md +++ b/plugins/dynatrace/api-report.md @@ -7,12 +7,13 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public export const dynatracePlugin: BackstagePlugin<{}, {}, {}>; // @public -export const DynatraceTab: () => JSX.Element; +export const DynatraceTab: () => JSX_2.Element; // @public export const isDynatraceAvailable: (entity: Entity) => boolean; diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json index 69ef692ee2..c50d22617c 100644 --- a/plugins/dynatrace/package.json +++ b/plugins/dynatrace/package.json @@ -35,6 +35,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -53,7 +54,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "express": "^4.18.1", "msw": "^1.0.0" }, diff --git a/plugins/entity-feedback-backend/README.md b/plugins/entity-feedback-backend/README.md index 99489e28b2..49a8ce20e0 100644 --- a/plugins/entity-feedback-backend/README.md +++ b/plugins/entity-feedback-backend/README.md @@ -4,6 +4,10 @@ Welcome to the entity-feedback backend plugin! ## Installation +Note: this plugin requires authentication and identity configured so Backstage can identify +which user has rated the entity. If you are using the guest identity provider which comes +out of the box, this plugin will not work when you test it. + ### Install the package ```bash diff --git a/plugins/entity-feedback-backend/src/service/router.ts b/plugins/entity-feedback-backend/src/service/router.ts index ea759c1639..4953c531b8 100644 --- a/plugins/entity-feedback-backend/src/service/router.ts +++ b/plugins/entity-feedback-backend/src/service/router.ts @@ -134,6 +134,9 @@ export async function createRouter( const user = await identity.getIdentity({ request: req }); const rating = req.body.rating; if (!user || !rating) { + logger.warn( + `Can't save rating because there is not enough info: user=${user}, rating=${rating}`, + ); res.status(400).end(); return; } @@ -188,6 +191,7 @@ export async function createRouter( const { response, comments, consent } = req.body; if (!user) { + logger.warn(`Could not identify user to save responses, user=${user}`); res.status(400).end(); return; } diff --git a/plugins/entity-feedback/README.md b/plugins/entity-feedback/README.md index fdfa4761d6..c405bd4083 100644 --- a/plugins/entity-feedback/README.md +++ b/plugins/entity-feedback/README.md @@ -36,7 +36,11 @@ This plugin allows you give and view feedback on entities available in the Backs ## Setup -The following sections will help you get the Entity Feedback plugin setup and running +The following sections will help you get the Entity Feedback plugin setup and running. + +Note: this plugin requires authentication and identity configured so Backstage can identify +which user has rated the entity. If you are using the guest identity provider which comes +out of the box, this plugin will not work when you test it. ### Backend @@ -133,4 +137,4 @@ const groupPage = ( ); ``` -Note: For a full example of this you can look at [this EntityPage](../../packages/app/src/components/catalog/EntityPage.tsx) +Note: For a full example of this you can look at [this EntityPage](../../packages/app/src/components/catalog/EntityPage.tsx). diff --git a/plugins/entity-feedback/api-report.md b/plugins/entity-feedback/api-report.md index 696536aef4..d61394ab9a 100644 --- a/plugins/entity-feedback/api-report.md +++ b/plugins/entity-feedback/api-report.md @@ -3,8 +3,6 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; @@ -14,6 +12,7 @@ import { FeedbackResponse } from '@backstage/plugin-entity-feedback-common'; import { FetchApi } from '@backstage/core-plugin-api'; import { Rating } from '@backstage/plugin-entity-feedback-common'; import { Ratings } from '@backstage/plugin-entity-feedback-common'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -84,18 +83,18 @@ export interface EntityFeedbackResponse { } // @public (undocumented) -export const EntityFeedbackResponseContent: () => JSX.Element; +export const EntityFeedbackResponseContent: () => React_2.JSX.Element; // @public (undocumented) -export const EntityLikeDislikeRatingsCard: () => JSX.Element; +export const EntityLikeDislikeRatingsCard: () => React_2.JSX.Element; // @public (undocumented) -export const EntityStarredRatingsCard: () => JSX.Element; +export const EntityStarredRatingsCard: () => React_2.JSX.Element; // @public (undocumented) export const FeedbackResponseDialog: ( props: FeedbackResponseDialogProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface FeedbackResponseDialogProps { @@ -114,7 +113,7 @@ export interface FeedbackResponseDialogProps { // @public (undocumented) export const FeedbackResponseTable: ( props: FeedbackResponseTableProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface FeedbackResponseTableProps { @@ -127,7 +126,7 @@ export interface FeedbackResponseTableProps { // @public (undocumented) export const LikeDislikeButtons: ( props: LikeDislikeButtonsProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface LikeDislikeButtonsProps { @@ -142,7 +141,7 @@ export interface LikeDislikeButtonsProps { // @public (undocumented) export const LikeDislikeRatingsTable: ( props: LikeDislikeRatingsTableProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface LikeDislikeRatingsTableProps { @@ -157,7 +156,7 @@ export interface LikeDislikeRatingsTableProps { // @public (undocumented) export const StarredRatingButtons: ( props: StarredRatingButtonsProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface StarredRatingButtonsProps { @@ -174,7 +173,7 @@ export interface StarredRatingButtonsProps { // @public (undocumented) export const StarredRatingsTable: ( props: StarredRatingsTableProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) export interface StarredRatingsTableProps { diff --git a/plugins/entity-validation/api-report.md b/plugins/entity-validation/api-report.md index 2fccbf9471..48b309cfaa 100644 --- a/plugins/entity-validation/api-report.md +++ b/plugins/entity-validation/api-report.md @@ -6,13 +6,14 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const EntityValidationPage: (props: { defaultYaml?: string | undefined; defaultLocation?: string | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const entityValidationPlugin: BackstagePlugin< diff --git a/plugins/entity-validation/package.json b/plugins/entity-validation/package.json index 7501c99613..6d63f3904d 100644 --- a/plugins/entity-validation/package.json +++ b/plugins/entity-validation/package.json @@ -43,6 +43,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@react-hookz/web": "^20.0.0", + "@types/react": "^16.13.1 || ^17.0.0", "@uiw/react-codemirror": "^4.9.3", "lodash": "^4.17.21", "react-use": "^17.2.4", @@ -63,7 +64,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts index a76817c3c0..57e1831667 100644 --- a/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts +++ b/plugins/events-backend-module-aws-sqs/src/service/eventsModuleAwsSqsConsumingEventPublisher.test.ts @@ -14,11 +14,6 @@ * limitations under the License. */ -import { - coreServices, - createServiceFactory, - SchedulerService, -} from '@backstage/backend-plugin-api'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { eventsExtensionPoint } from '@backstage/plugin-events-node/alpha'; import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils'; @@ -34,9 +29,7 @@ describe('eventsModuleAwsSqsConsumingEventPublisher', () => { }, }; - const scheduler = { - scheduleTask: jest.fn(), - } as unknown as SchedulerService; + const scheduler = mockServices.scheduler.mock(); await startTestBackend({ extensionPoints: [[eventsExtensionPoint, extensionPoint]], @@ -68,11 +61,7 @@ describe('eventsModuleAwsSqsConsumingEventPublisher', () => { }, }, }), - createServiceFactory({ - service: coreServices.scheduler, - deps: {}, - factory: () => scheduler, - }), + scheduler.factory, ], }); diff --git a/plugins/example-todo-list/api-report.md b/plugins/example-todo-list/api-report.md index 68b3ffd73e..1b7aa49a19 100644 --- a/plugins/example-todo-list/api-report.md +++ b/plugins/example-todo-list/api-report.md @@ -6,10 +6,11 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public -export const TodoListPage: () => JSX.Element; +export const TodoListPage: () => JSX_2.Element; // @public export const todoListPlugin: BackstagePlugin< diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index 70ad479841..193efb044d 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -16,6 +16,8 @@ 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 { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -36,15 +38,17 @@ export const catalogEntityRouteRef: ExternalRouteRef< export const CatalogKindExploreContent: (props: { title?: string; kind: string; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) -export const DomainCard: (props: { entity: DomainEntity }) => JSX.Element; +export const DomainCard: (props: { + entity: DomainEntity; +}) => React_2.JSX.Element; // @public (undocumented) export const DomainExplorerContent: (props: { title?: string | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public export interface ExploreApi { @@ -67,7 +71,7 @@ export class ExploreClient implements ExploreApi { // @public export const ExploreLayout: { - (props: ExploreLayoutProps): JSX.Element; + (props: ExploreLayoutProps): default_2.JSX.Element; Route: (props: SubRoute) => null; }; @@ -79,7 +83,7 @@ export type ExploreLayoutProps = { }; // @public (undocumented) -export const ExplorePage: () => JSX.Element; +export const ExplorePage: () => JSX_2.Element; // @public (undocumented) const explorePlugin: BackstagePlugin< @@ -107,7 +111,7 @@ export const exploreRouteRef: RouteRef; // @public (undocumented) export const GroupsExplorerContent: (props: { title?: string | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export type SubRoute = { @@ -125,7 +129,7 @@ export type SubRoute = { // @public (undocumented) export const ToolExplorerContent: (props: { title?: string | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const ToolSearchResultListItem: ( diff --git a/plugins/firehydrant/api-report.md b/plugins/firehydrant/api-report.md index 0ff5bbc62a..76ccd8f019 100644 --- a/plugins/firehydrant/api-report.md +++ b/plugins/firehydrant/api-report.md @@ -7,10 +7,11 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const FirehydrantCard: () => JSX.Element; +export const FirehydrantCard: () => JSX_2.Element; // @public (undocumented) export const firehydrantPlugin: BackstagePlugin< diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 287d40238a..e9459d547a 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -55,7 +56,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/fossa/api-report.md b/plugins/fossa/api-report.md index 45f0746ac3..ac76ff6af6 100644 --- a/plugins/fossa/api-report.md +++ b/plugins/fossa/api-report.md @@ -7,15 +7,16 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const EntityFossaCard: (props: { variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const FossaPage: (props: FossaPageProps) => JSX.Element; +export const FossaPage: (props: FossaPageProps) => JSX_2.Element; // @public (undocumented) export type FossaPageProps = { diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 719658d33f..f614fbbdf2 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -42,6 +42,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "luxon": "^3.0.0", "p-limit": "^3.0.2", @@ -62,7 +63,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/gcalendar/api-report.md b/plugins/gcalendar/api-report.md index 8c86e4d2c9..3f1d42d239 100644 --- a/plugins/gcalendar/api-report.md +++ b/plugins/gcalendar/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -55,7 +56,7 @@ export const gcalendarPlugin: BackstagePlugin< >; // @public (undocumented) -export const HomePageCalendar: () => JSX.Element; +export const HomePageCalendar: () => JSX_2.Element; // @public (undocumented) export enum ResponseStatus { diff --git a/plugins/gcalendar/package.json b/plugins/gcalendar/package.json index 9c9a8e6bbe..a28b26aacc 100644 --- a/plugins/gcalendar/package.json +++ b/plugins/gcalendar/package.json @@ -37,6 +37,7 @@ "@material-ui/lab": "4.0.0-alpha.61", "@maxim_mazurok/gapi.client.calendar": "^3.0.20220408", "@tanstack/react-query": "^4.1.3", + "@types/react": "^16.13.1 || ^17.0.0", "classnames": "^2.3.1", "cross-fetch": "^3.1.5", "dompurify": "^2.3.6", @@ -61,7 +62,6 @@ "@testing-library/user-event": "^14.0.0", "@types/dompurify": "^2.3.3", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "@types/sanitize-html": "^2.6.2", "msw": "^1.0.0" }, diff --git a/plugins/gcp-projects/api-report.md b/plugins/gcp-projects/api-report.md index 5b7604c935..fd156687c0 100644 --- a/plugins/gcp-projects/api-report.md +++ b/plugins/gcp-projects/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -40,7 +41,7 @@ export class GcpClient implements GcpApi { } // @public (undocumented) -export const GcpProjectsPage: () => JSX.Element; +export const GcpProjectsPage: () => JSX_2.Element; // @public (undocumented) const gcpProjectsPlugin: BackstagePlugin< diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 7915db1dd0..0c21e712b2 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -39,7 +39,8 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", - "@react-hookz/web": "^20.0.0" + "@react-hookz/web": "^20.0.0", + "@types/react": "^16.13.1 || ^17.0.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", @@ -56,7 +57,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/git-release-manager/api-report.md b/plugins/git-release-manager/api-report.md index edc2e56ad9..a3215041f0 100644 --- a/plugins/git-release-manager/api-report.md +++ b/plugins/git-release-manager/api-report.md @@ -142,7 +142,7 @@ export interface CreateRcOnSuccessArgs { // Warning: (ae-missing-release-tag) "Differ" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const Differ: ({ current, next, icon }: DifferProps) => JSX.Element; +const Differ: ({ current, next, icon }: DifferProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "DISABLE_CACHE" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -156,7 +156,7 @@ const DISABLE_CACHE: { // Warning: (ae-missing-release-tag) "Divider" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const Divider: () => JSX.Element; +const Divider: () => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "SemverTagParts" needs to be exported by the entry point index.d.ts // @@ -285,7 +285,9 @@ declare namespace helpers { } // @public (undocumented) -const InfoCardPlus: (props: { children?: React_2.ReactNode }) => JSX.Element; +const InfoCardPlus: (props: { + children?: React_2.ReactNode; +}) => React_2.JSX.Element; declare namespace internals { export { components, constants, helpers, testHelpers }; @@ -311,7 +313,7 @@ function isProjectValid(project: any): project is Project; function LinearProgressWithLabel(props: { progress: number; responseSteps: ResponseStep[]; -}): JSX.Element; +}): React_2.JSX.Element; // Warning: (ae-missing-release-tag) "MOCK_RELEASE_BRANCH_NAME_CALVER" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -523,7 +525,7 @@ const mockUsername = 'mock_username'; // Warning: (ae-missing-release-tag) "NoLatestRelease" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const NoLatestRelease: () => JSX.Element; +const NoLatestRelease: () => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "PatchOnSuccessArgs" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -601,7 +603,7 @@ const ResponseStepDialog: ({ progress, responseSteps, title, -}: DialogProps) => JSX.Element; +}: DialogProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ResponseStepListProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ResponseStepList" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -613,7 +615,7 @@ const ResponseStepList: ({ loading, denseList, children, -}: PropsWithChildren) => JSX.Element; +}: PropsWithChildren) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ResponseStepListItemProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ResponseStepListItem" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -622,7 +624,7 @@ const ResponseStepList: ({ const ResponseStepListItem: ({ responseStep, animationDelay, -}: ResponseStepListItemProps) => JSX.Element; +}: ResponseStepListItemProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "SEMVER_PARTS" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/github-actions/api-report.md b/plugins/github-actions/api-report.md index 0c9d06d90e..e7c6114c93 100644 --- a/plugins/github-actions/api-report.md +++ b/plugins/github-actions/api-report.md @@ -10,6 +10,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RestEndpointMethodTypes } from '@octokit/rest'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmAuthApi } from '@backstage/integration-react'; @@ -27,19 +29,19 @@ export enum BuildStatus { } // @public (undocumented) -export const EntityGithubActionsContent: () => JSX.Element; +export const EntityGithubActionsContent: () => JSX_2.Element; // @public (undocumented) export const EntityLatestGithubActionRunCard: (props: { branch: string; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityLatestGithubActionsForBranchCard: (props: { branch: string; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityRecentGithubActionsRunsCard: (props: { @@ -47,7 +49,7 @@ export const EntityRecentGithubActionsRunsCard: (props: { dense?: boolean | undefined; limit?: number | undefined; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const GITHUB_ACTIONS_ANNOTATION = 'github.com/project-slug'; @@ -208,13 +210,13 @@ export type Jobs = { export const LatestWorkflowRunCard: (props: { branch: string; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const LatestWorkflowsForBranchCard: (props: { branch: string; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const RecentWorkflowRunsCard: (props: { @@ -222,10 +224,10 @@ export const RecentWorkflowRunsCard: (props: { dense?: boolean; limit?: number; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // @public (undocumented) export type Step = { diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index f951503e9e..ac0fc2a1b1 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -45,6 +45,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@octokit/rest": "^19.0.3", + "@types/react": "^16.13.1 || ^17.0.0", "git-url-parse": "^13.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" @@ -64,7 +65,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/github-deployments/api-report.md b/plugins/github-deployments/api-report.md index 82d2222127..669bcd74ba 100644 --- a/plugins/github-deployments/api-report.md +++ b/plugins/github-deployments/api-report.md @@ -7,6 +7,8 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { TableColumn } from '@backstage/core-components'; // @public (undocumented) @@ -14,7 +16,7 @@ export const EntityGithubDeploymentsCard: (props: { last?: number | undefined; lastStatuses?: number | undefined; columns?: TableColumn[] | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug'; @@ -50,7 +52,7 @@ export const GithubDeploymentsTable: { isLoading: boolean; reload: () => void; columns: TableColumn[]; - }): JSX.Element; + }): React_2.JSX.Element; columns: Readonly<{ createEnvironmentColumn(): TableColumn; createStatusColumn(): TableColumn; diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 05b85aaa60..3a70d51399 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -41,6 +41,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@octokit/graphql": "^5.0.0", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -59,7 +60,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/github-issues/api-report.md b/plugins/github-issues/api-report.md index 4e57eeda46..3c7b616f82 100644 --- a/plugins/github-issues/api-report.md +++ b/plugins/github-issues/api-report.md @@ -6,6 +6,7 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -17,7 +18,7 @@ export interface GithubIssuesByRepoOptions { } // @public (undocumented) -export const GithubIssuesCard: (props: GithubIssuesProps) => JSX.Element; +export const GithubIssuesCard: (props: GithubIssuesProps) => JSX_2.Element; // @public (undocumented) export interface GithubIssuesFilters { @@ -44,7 +45,7 @@ export interface GithubIssuesOrdering { } // @public (undocumented) -export const GithubIssuesPage: (props: GithubIssuesProps) => JSX.Element; +export const GithubIssuesPage: (props: GithubIssuesProps) => JSX_2.Element; // @public (undocumented) export const githubIssuesPlugin: BackstagePlugin< diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json index 7cedc25632..1467bc4050 100644 --- a/plugins/github-issues/package.json +++ b/plugins/github-issues/package.json @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.4", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "octokit": "^2.0.4", "react-use": "^17.4.0" @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/github-pull-requests-board/api-report.md b/plugins/github-pull-requests-board/api-report.md index 81097118af..24a8f4d926 100644 --- a/plugins/github-pull-requests-board/api-report.md +++ b/plugins/github-pull-requests-board/api-report.md @@ -5,10 +5,12 @@ ```ts /// +import { JSX as JSX_2 } from 'react'; + // @public (undocumented) export const EntityTeamPullRequestsCard: ( props: EntityTeamPullRequestsCardProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export interface EntityTeamPullRequestsCardProps { @@ -19,7 +21,7 @@ export interface EntityTeamPullRequestsCardProps { // @public (undocumented) export const EntityTeamPullRequestsContent: ( props: EntityTeamPullRequestsContentProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export interface EntityTeamPullRequestsContentProps { diff --git a/plugins/github-pull-requests-board/package.json b/plugins/github-pull-requests-board/package.json index ae27d11e50..90402db5b5 100644 --- a/plugins/github-pull-requests-board/package.json +++ b/plugins/github-pull-requests-board/package.json @@ -44,6 +44,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@octokit/rest": "^19.0.3", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "p-limit": "^4.0.0", "react-use": "^17.2.4" @@ -62,7 +63,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/gitops-profiles/api-report.md b/plugins/gitops-profiles/api-report.md index 9449417bba..3dc3c9add2 100644 --- a/plugins/gitops-profiles/api-report.md +++ b/plugins/gitops-profiles/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -105,13 +106,13 @@ export type GitOpsApi = { export const gitOpsApiRef: ApiRef; // @public (undocumented) -export const GitopsProfilesClusterListPage: () => JSX.Element; +export const GitopsProfilesClusterListPage: () => JSX_2.Element; // @public (undocumented) -export const GitopsProfilesClusterPage: () => JSX.Element; +export const GitopsProfilesClusterPage: () => JSX_2.Element; // @public (undocumented) -export const GitopsProfilesCreatePage: () => JSX.Element; +export const GitopsProfilesCreatePage: () => JSX_2.Element; // @public (undocumented) const gitopsProfilesPlugin: BackstagePlugin< diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 7fed1cf06a..c4b99ff2e2 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -40,6 +40,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/graphiql/api-report.md b/plugins/graphiql/api-report.md index ea765d4d2b..788551476a 100644 --- a/plugins/graphiql/api-report.md +++ b/plugins/graphiql/api-report.md @@ -9,7 +9,9 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ErrorApi } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -36,7 +38,7 @@ export type GithubEndpointConfig = { export const GraphiQLIcon: IconComponent; // @public (undocumented) -export const GraphiQLPage: () => JSX.Element; +export const GraphiQLPage: () => JSX_2.Element; // @public (undocumented) const graphiqlPlugin: BackstagePlugin<{}, {}, {}>; @@ -73,7 +75,7 @@ export class GraphQLEndpoints implements GraphQLBrowseApi { } // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index aacdb84d76..54e01003a6 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -38,6 +38,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "graphiql": "^1.5.12", "graphql": "^16.0.0", "graphql-ws": "^5.4.1", @@ -59,7 +60,6 @@ "@testing-library/user-event": "^14.0.0", "@types/codemirror": "^5.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/graphql-voyager/api-report.md b/plugins/graphql-voyager/api-report.md index 9b488f9356..257a8ca7b7 100644 --- a/plugins/graphql-voyager/api-report.md +++ b/plugins/graphql-voyager/api-report.md @@ -7,6 +7,8 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { VoyagerDisplayOptions } from 'graphql-voyager/typings/components/Voyager'; import { WorkerCallback } from 'graphql-voyager/typings/utils/types'; @@ -45,7 +47,7 @@ export class GraphQLVoyagerEndpoints implements GraphQLVoyagerApi { // @public (undocumented) export const GraphqlVoyagerPage: ( props: GraphQLVoyagerPageProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type GraphQLVoyagerPageProps = { @@ -65,7 +67,7 @@ export const graphqlVoyagerPlugin: BackstagePlugin< export const introspectionQuery: string; // @public (undocumented) -export const Router: (props: GraphQLVoyagerPageProps) => JSX.Element; +export const Router: (props: GraphQLVoyagerPageProps) => React_2.JSX.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/graphql-voyager/package.json b/plugins/graphql-voyager/package.json index 814ff95e33..cb1ba1126d 100644 --- a/plugins/graphql-voyager/package.json +++ b/plugins/graphql-voyager/package.json @@ -35,6 +35,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "graphql-voyager": "^1.0.0-rc.31", "react-use": "^17.2.4" }, @@ -53,7 +54,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/home-react/api-report.md b/plugins/home-react/api-report.md index 4806cf7be0..268f598a63 100644 --- a/plugins/home-react/api-report.md +++ b/plugins/home-react/api-report.md @@ -3,9 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { Extension } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RJSFSchema } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; @@ -61,7 +60,7 @@ export function createCardExtension(options: { description?: string; layout?: CardLayout; settings?: CardSettings; -}): Extension<(props: CardExtensionProps) => JSX.Element>; +}): Extension<(props: CardExtensionProps) => React_2.JSX.Element>; // @public (undocumented) export type RendererProps = { @@ -74,5 +73,5 @@ export const SettingsModal: (props: { close: Function; componentName?: string; children: JSX.Element; -}) => JSX.Element; +}) => React_2.JSX.Element; ``` diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index 182c5d77f8..85e3cca440 100644 --- a/plugins/home-react/package.json +++ b/plugins/home-react/package.json @@ -37,7 +37,8 @@ "@backstage/core-plugin-api": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@rjsf/utils": "5.7.3" + "@rjsf/utils": "5.7.3", + "@types/react": "^16.13.1 || ^17.0.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0", @@ -54,7 +55,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "@types/react-grid-layout": "^1.3.2", "cross-fetch": "^3.1.5", "msw": "^1.0.0" diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md index 32c1c0eb9d..c5e458f78f 100644 --- a/plugins/home/api-report.md +++ b/plugins/home/api-report.md @@ -13,6 +13,7 @@ import { CardSettings as CardSettings_2 } from '@backstage/plugin-home-react'; import { ComponentParts as ComponentParts_2 } from '@backstage/plugin-home-react'; import { ComponentRenderer as ComponentRenderer_2 } from '@backstage/plugin-home-react'; import { createCardExtension as createCardExtension_2 } from '@backstage/plugin-home-react'; +import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; @@ -48,7 +49,7 @@ export const ComponentAccordion: (props: { Actions?: (() => JSX.Element) | undefined; Settings?: (() => JSX.Element) | undefined; ContextProvider?: ((props: any) => JSX.Element) | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public @deprecated (undocumented) export type ComponentParts = ComponentParts_2; @@ -61,7 +62,7 @@ export const ComponentTab: (props: { title: string; Content: () => JSX.Element; ContextProvider?: ((props: any) => JSX.Element) | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const ComponentTabs: (props: { @@ -70,7 +71,7 @@ export const ComponentTabs: (props: { label: string; Component: () => JSX.Element; }[]; -}) => JSX.Element; +}) => JSX_2.Element; // @public @deprecated (undocumented) export const createCardExtension: typeof createCardExtension_2; @@ -78,7 +79,7 @@ export const createCardExtension: typeof createCardExtension_2; // @public export const CustomHomepageGrid: ( props: CustomHomepageGridProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type CustomHomepageGridProps = { @@ -100,36 +101,36 @@ export type CustomHomepageGridProps = { export const HeaderWorldClock: (props: { clockConfigs: ClockConfig[]; customTimeFormat?: Intl.DateTimeFormatOptions | undefined; -}) => JSX.Element | null; +}) => JSX_2.Element | null; // @public export const HomePageCompanyLogo: (props: { logo?: ReactNode; className?: string | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const HomepageCompositionRoot: (props: { title?: string | undefined; children?: ReactNode; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const HomePageRandomJoke: ( props: CardExtensionProps_2<{ defaultCategory?: 'any' | 'programming' | undefined; }>, -) => JSX.Element; +) => JSX_2.Element; // @public export const HomePageStarredEntities: ( props: CardExtensionProps_2, -) => JSX.Element; +) => JSX_2.Element; // @public export const HomePageToolkit: ( props: CardExtensionProps_2, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export const homePlugin: BackstagePlugin< @@ -158,7 +159,7 @@ export const SettingsModal: (props: { close: Function; componentName?: string | undefined; children: JSX.Element; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const TemplateBackstageLogo: (props: { @@ -166,10 +167,10 @@ export const TemplateBackstageLogo: (props: { svg: string; path: string; }; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) -export const TemplateBackstageLogoIcon: () => JSX.Element; +export const TemplateBackstageLogoIcon: () => React_2.JSX.Element; // @public (undocumented) export type Tool = { @@ -186,7 +187,7 @@ export type ToolkitContentProps = { // @public export const WelcomeTitle: ({ language, -}: WelcomeTitleLanguageProps) => JSX.Element; +}: WelcomeTitleLanguageProps) => JSX_2.Element; // @public (undocumented) export type WelcomeTitleLanguageProps = { diff --git a/plugins/ilert/api-report.md b/plugins/ilert/api-report.md index 6cc0c12a8f..b2081e049e 100644 --- a/plugins/ilert/api-report.md +++ b/plugins/ilert/api-report.md @@ -11,6 +11,8 @@ import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IconComponent } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -302,7 +304,7 @@ export type AlertStatus = typeof PENDING | typeof ACCEPTED | typeof RESOLVED; export const DEGRADED = 'DEGRADED'; // @public (undocumented) -export const EntityILertCard: () => JSX.Element; +export const EntityILertCard: () => JSX_2.Element; // @public (undocumented) export interface EscalationPolicy { @@ -442,7 +444,7 @@ export interface ILertApi { export const ilertApiRef: ApiRef; // @public (undocumented) -export const ILertCard: () => JSX.Element; +export const ILertCard: () => React_2.JSX.Element; // @public (undocumented) export class ILertClient implements ILertApi { @@ -530,7 +532,7 @@ export class ILertClient implements ILertApi { export const ILertIcon: IconComponent; // @public (undocumented) -export const ILertPage: () => JSX.Element; +export const ILertPage: () => JSX_2.Element; // @public (undocumented) const ilertPlugin: BackstagePlugin< @@ -649,7 +651,7 @@ export interface Responder { } // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // @public (undocumented) export interface Schedule { diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index c9a5320941..cda4ab0a8a 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -40,6 +40,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@material-ui/pickers": "^3.3.10", + "@types/react": "^16.13.1 || ^17.0.0", "humanize-duration": "^3.26.0", "luxon": "^3.0.0", "prop-types": "^15.7.2", @@ -60,7 +61,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/jenkins/api-report.md b/plugins/jenkins/api-report.md index a2ae16988b..0782f0abd2 100644 --- a/plugins/jenkins/api-report.md +++ b/plugins/jenkins/api-report.md @@ -12,16 +12,18 @@ import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EntityJenkinsContent: () => JSX.Element; +export const EntityJenkinsContent: () => JSX_2.Element; // @public (undocumented) export const EntityLatestJenkinsRunCard: (props: { branch: string; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) const isJenkinsAvailable: (entity: Entity) => boolean; @@ -109,7 +111,7 @@ export { jenkinsPlugin as plugin }; export const LatestRunCard: (props: { branch: string; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "LEGACY_JENKINS_ANNOTATION" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -119,5 +121,5 @@ export const LEGACY_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; // Warning: (ae-missing-release-tag) "Router" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index ddc100d03e..29c6da9eda 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -43,6 +43,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -61,7 +62,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "@types/testing-library__jest-dom": "^5.9.1", "cross-fetch": "^3.1.5", "msw": "^1.0.0" diff --git a/plugins/kafka/api-report.md b/plugins/kafka/api-report.md index 18b51b77fa..990a4da295 100644 --- a/plugins/kafka/api-report.md +++ b/plugins/kafka/api-report.md @@ -7,10 +7,12 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EntityKafkaContent: () => JSX.Element; +export const EntityKafkaContent: () => JSX_2.Element; // @public (undocumented) const isPluginApplicableToEntity: (entity: Entity) => boolean; @@ -33,5 +35,5 @@ export { kafkaPlugin }; export { kafkaPlugin as plugin }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index b2f8b6fe64..b599d7d356 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -39,6 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -57,7 +58,6 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "jest-when": "^3.1.0", "msw": "^1.0.0" }, diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index 9d67ecba83..3c91de02ac 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -30,6 +30,7 @@ import { OpenIdConnectApi } from '@backstage/core-plugin-api'; import { Pod } from 'kubernetes-models/v1/Pod'; import { Pod as Pod_2 } from 'kubernetes-models/v1'; import { default as React_2 } from 'react'; +import * as React_3 from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TypeMeta } from '@kubernetes-models/base'; import { V1ConfigMap } from '@kubernetes/client-node'; @@ -52,7 +53,7 @@ import { WorkloadsByEntityRequest } from '@backstage/plugin-kubernetes-common'; export const Cluster: ({ clusterObjects, podsWithErrors, -}: ClusterProps) => JSX.Element; +}: ClusterProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "ClusterContext" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -110,13 +111,13 @@ export interface ContainerScope extends PodScope { // Warning: (ae-missing-release-tag) "CronJobsAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const CronJobsAccordions: ({}: CronJobsAccordionsProps) => JSX.Element; +export const CronJobsAccordions: ({}: CronJobsAccordionsProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "CustomResourcesProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "CustomResources" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const CustomResources: ({}: CustomResourcesProps) => JSX.Element; +export const CustomResources: ({}: CustomResourcesProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "DeploymentResources" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -174,7 +175,9 @@ export type EntityKubernetesContentProps = { }; // @public -export const ErrorList: ({ podAndErrors }: ErrorListProps) => JSX.Element; +export const ErrorList: ({ + podAndErrors, +}: ErrorListProps) => React_2.JSX.Element; // @public export interface ErrorListProps { @@ -195,7 +198,7 @@ export const ErrorPanel: ({ entityName, errorMessage, clustersWithErrors, -}: ErrorPanelProps) => JSX.Element; +}: ErrorPanelProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ErrorReportingProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ErrorReporting" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -203,7 +206,7 @@ export const ErrorPanel: ({ // @public (undocumented) export const ErrorReporting: ({ detectedErrors, -}: ErrorReportingProps) => JSX.Element; +}: ErrorReportingProps) => React_3.JSX.Element; // @public export type ErrorSeverity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; @@ -214,13 +217,13 @@ export const Events: ({ namespace, clusterName, warningEventsOnly, -}: EventsProps) => JSX.Element; +}: EventsProps) => React_2.JSX.Element; // @public export const EventsContent: ({ events, warningEventsOnly, -}: EventsContentProps) => JSX.Element; +}: EventsContentProps) => React_2.JSX.Element; // @public export interface EventsContentProps { @@ -323,13 +326,13 @@ export const HorizontalPodAutoscalerDrawer: (props: { hpa: V1HorizontalPodAutoscaler; expanded?: boolean; children?: React_2.ReactNode; -}) => JSX.Element; +}) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "IngressesAccordionsProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IngressesAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const IngressesAccordions: ({}: IngressesAccordionsProps) => JSX.Element; +export const IngressesAccordions: ({}: IngressesAccordionsProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "isKubernetesAvailable" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -340,7 +343,9 @@ export const isKubernetesAvailable: (entity: Entity) => boolean; // Warning: (ae-missing-release-tag) "JobsAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const JobsAccordions: ({ jobs }: JobsAccordionsProps) => JSX.Element; +export const JobsAccordions: ({ + jobs, +}: JobsAccordionsProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "KubernetesApi" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -464,7 +469,7 @@ export class KubernetesBackendClient implements KubernetesApi { export const KubernetesContent: ({ entity, refreshIntervalMs, -}: KubernetesContentProps) => JSX.Element; +}: KubernetesContentProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "KubernetesDrawerProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "KubernetesDrawer" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -476,7 +481,7 @@ export const KubernetesDrawer: ({ drawerContentsHeader, kubernetesObject, children, -}: KubernetesDrawerProps) => JSX.Element; +}: KubernetesDrawerProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "KubernetesDrawerContentProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "KubernetesDrawerContent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -487,7 +492,7 @@ export const KubernetesDrawerContent: ({ header, kubernetesObject, close, -}: KubernetesDrawerContentProps) => JSX.Element; +}: KubernetesDrawerContentProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "KubernetesObjects" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -586,7 +591,7 @@ export const KubernetesStructuredMetadataTableDrawer: < buttonVariant, expanded, children, -}: KubernetesStructuredMetadataTableDrawerProps) => JSX.Element; +}: KubernetesStructuredMetadataTableDrawerProps) => React_2.JSX.Element; // Warning: (ae-forgotten-export) The symbol "ErrorPanelProps_2" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "LinkErrorPanel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -595,12 +600,12 @@ export const KubernetesStructuredMetadataTableDrawer: < export const LinkErrorPanel: ({ cluster, errorMessage, -}: ErrorPanelProps_2) => JSX.Element; +}: ErrorPanelProps_2) => React_2.JSX.Element; // @public export const PendingPodContent: ({ pod, -}: PendingPodContentProps) => JSX.Element; +}: PendingPodContentProps) => React_2.JSX.Element; // @public export interface PendingPodContentProps { @@ -621,7 +626,10 @@ export interface PodAndErrors { // Warning: (ae-forgotten-export) The symbol "PodDrawerProps" needs to be exported by the entry point index.d.ts // // @public -export const PodDrawer: ({ podAndErrors, open }: PodDrawerProps) => JSX.Element; +export const PodDrawer: ({ + podAndErrors, + open, +}: PodDrawerProps) => React_2.JSX.Element; // @public export const PodLogs: React_2.FC; @@ -629,7 +637,7 @@ export const PodLogs: React_2.FC; // @public export const PodLogsDialog: ({ containerScope, -}: PodLogsDialogProps) => JSX.Element; +}: PodLogsDialogProps) => React_2.JSX.Element; // @public export interface PodLogsDialogProps { @@ -692,7 +700,7 @@ export interface PodScope { export const PodsTable: ({ pods, extraColumns, -}: PodsTablesProps) => JSX.Element; +}: PodsTablesProps) => React_2.JSX.Element; // @public export interface ResourceRef { @@ -715,12 +723,14 @@ export const ResourceUtilization: ({ usage, total, totalFormatted, -}: ResourceUtilizationProps) => JSX.Element; +}: ResourceUtilizationProps) => React_2.JSX.Element; // Warning: (ae-missing-release-tag) "Router" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const Router: (props: { refreshIntervalMs?: number }) => JSX.Element; +export const Router: (props: { + refreshIntervalMs?: number; +}) => React_2.JSX.Element; // @public export class ServerSideKubernetesAuthProvider @@ -738,7 +748,7 @@ export class ServerSideKubernetesAuthProvider // Warning: (ae-missing-release-tag) "ServicesAccordions" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ServicesAccordions: ({}: ServicesAccordionsProps) => JSX.Element; +export const ServicesAccordions: ({}: ServicesAccordionsProps) => React_2.JSX.Element; // @public export const useCustomResources: ( diff --git a/plugins/lighthouse/api-report.md b/plugins/lighthouse/api-report.md index 4ab851472a..9794fe1728 100644 --- a/plugins/lighthouse/api-report.md +++ b/plugins/lighthouse/api-report.md @@ -9,20 +9,22 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; import { LighthouseApi } from '@backstage/plugin-lighthouse-common'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EmbeddedRouter: () => JSX.Element; +export const EmbeddedRouter: () => React_2.JSX.Element; // @public (undocumented) export const EntityLastLighthouseAuditCard: (props: { dense?: boolean | undefined; variant?: InfoCardVariants | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const EntityLighthouseContent: () => JSX.Element; +export const EntityLighthouseContent: () => JSX_2.Element; // @public (undocumented) const isLighthouseAvailable: (entity: Entity) => boolean; @@ -33,13 +35,13 @@ export { isLighthouseAvailable as isPluginApplicableToEntity }; export const LastLighthouseAuditCard: (props: { dense?: boolean; variant?: InfoCardVariants; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const lighthouseApiRef: ApiRef; // @public (undocumented) -export const LighthousePage: () => JSX.Element; +export const LighthousePage: () => JSX_2.Element; // @public (undocumented) const lighthousePlugin: BackstagePlugin< @@ -54,5 +56,5 @@ export { lighthousePlugin }; export { lighthousePlugin as plugin }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 990e789613..edac0478af 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -43,6 +43,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -61,7 +62,6 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/linguist-backend/src/service/router.ts b/plugins/linguist-backend/src/service/router.ts index 02ff05f398..d29943c38b 100644 --- a/plugins/linguist-backend/src/service/router.ts +++ b/plugins/linguist-backend/src/service/router.ts @@ -150,9 +150,8 @@ export async function createRouterFromConfig(routerOptions: RouterOptions) { ); } pluginOptions.batchSize = config.getOptionalNumber('linguist.batchSize'); - pluginOptions.useSourceLocation = config.getBoolean( - 'linguist.useSourceLocation', - ); + pluginOptions.useSourceLocation = + config.getOptionalBoolean('linguist.useSourceLocation') ?? false; pluginOptions.age = config.getOptionalConfig('linguist.age') as | HumanDuration | undefined; diff --git a/plugins/linguist/api-report.md b/plugins/linguist/api-report.md index 469b3b93c0..5182ce12f6 100644 --- a/plugins/linguist/api-report.md +++ b/plugins/linguist/api-report.md @@ -7,9 +7,10 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) -export const EntityLinguistCard: () => JSX.Element; +export const EntityLinguistCard: () => JSX_2.Element; // @public (undocumented) export const isLinguistAvailable: (entity: Entity) => boolean; diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json index 45a24fcd24..496e8e65d2 100644 --- a/plugins/linguist/package.json +++ b/plugins/linguist/package.json @@ -38,6 +38,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^2.0.2", "react-use": "^17.2.4", "slugify": "^1.6.4" @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/microsoft-calendar/api-report.md b/plugins/microsoft-calendar/api-report.md index d6839f19ea..fad48e5de0 100644 --- a/plugins/microsoft-calendar/api-report.md +++ b/plugins/microsoft-calendar/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { Calendar } from '@microsoft/microsoft-graph-types'; import { Event as Event_2 } from '@microsoft/microsoft-graph-types'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { OAuthApi } from '@backstage/core-plugin-api'; // @public (undocumented) @@ -33,7 +34,7 @@ export class MicrosoftCalendarApiClient { export const microsoftCalendarApiRef: ApiRef; // @public (undocumented) -export const MicrosoftCalendarCard: () => JSX.Element; +export const MicrosoftCalendarCard: () => JSX_2.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/microsoft-calendar/package.json b/plugins/microsoft-calendar/package.json index 5003f887c1..888d661aa8 100644 --- a/plugins/microsoft-calendar/package.json +++ b/plugins/microsoft-calendar/package.json @@ -55,6 +55,7 @@ "@material-ui/icons": "^4.9.1", "@microsoft/microsoft-graph-types": "^2.25.0", "@tanstack/react-query": "^4.1.3", + "@types/react": "^16.13.1 || ^17.0.0", "classnames": "^2.3.1", "dompurify": "^2.3.6", "lodash": "^4.17.21", @@ -77,7 +78,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/newrelic-dashboard/api-report.md b/plugins/newrelic-dashboard/api-report.md index 740fc46ef7..2e9b11a88e 100644 --- a/plugins/newrelic-dashboard/api-report.md +++ b/plugins/newrelic-dashboard/api-report.md @@ -7,6 +7,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public @@ -14,13 +15,13 @@ export const DashboardSnapshotComponent: (props: { guid: string; name: string; permalink: string; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) -export const EntityNewRelicDashboardCard: () => JSX.Element; +export const EntityNewRelicDashboardCard: () => JSX_2.Element; // @public (undocumented) -export const EntityNewRelicDashboardContent: () => JSX.Element; +export const EntityNewRelicDashboardContent: () => JSX_2.Element; // @public (undocumented) export const isNewRelicDashboardAvailable: (entity: Entity) => boolean; diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index ba12a51f71..3c23e04b77 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -36,6 +36,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -46,8 +47,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/dev-utils": "workspace:^", - "@testing-library/jest-dom": "^5.10.1", - "@types/react": "^16.13.1 || ^17.0.0" + "@testing-library/jest-dom": "^5.10.1" }, "files": [ "dist" diff --git a/plugins/newrelic/api-report.md b/plugins/newrelic/api-report.md index 302b7c918a..04fd229465 100644 --- a/plugins/newrelic/api-report.md +++ b/plugins/newrelic/api-report.md @@ -6,10 +6,11 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const NewRelicPage: () => JSX.Element; +export const NewRelicPage: () => JSX_2.Element; // @public (undocumented) const newRelicPlugin: BackstagePlugin< diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 7e49f1220b..ba2b6cb546 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -39,6 +39,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "parse-link-header": "^2.0.0", "react-use": "^17.2.4" }, @@ -59,7 +60,6 @@ "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", "@types/parse-link-header": "^2.0.1", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.2.3" }, diff --git a/plugins/nomad/api-report.md b/plugins/nomad/api-report.md index f000854374..c2d06b9892 100644 --- a/plugins/nomad/api-report.md +++ b/plugins/nomad/api-report.md @@ -7,13 +7,14 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public -export const EntityNomadAllocationListTable: () => JSX.Element; +export const EntityNomadAllocationListTable: () => JSX_2.Element; // @public -export const EntityNomadJobVersionListCard: () => JSX.Element | null; +export const EntityNomadJobVersionListCard: () => JSX_2.Element | null; // @public (undocumented) export const isNomadAllocationsAvailable: (entity: Entity) => boolean; diff --git a/plugins/nomad/package.json b/plugins/nomad/package.json index 68bad74c0e..3d555aedbf 100644 --- a/plugins/nomad/package.json +++ b/plugins/nomad/package.json @@ -37,6 +37,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.60", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.3.0", "react-use": "^17.2.4" }, diff --git a/plugins/octopus-deploy/api-report.md b/plugins/octopus-deploy/api-report.md index 53c92403bd..713d99aa8f 100644 --- a/plugins/octopus-deploy/api-report.md +++ b/plugins/octopus-deploy/api-report.md @@ -10,11 +10,12 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export const EntityOctopusDeployContent: (props: { defaultLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const isOctopusDeployAvailable: (entity: Entity) => boolean; diff --git a/plugins/octopus-deploy/package.json b/plugins/octopus-deploy/package.json index a715570fd0..2e7322d5ab 100644 --- a/plugins/octopus-deploy/package.json +++ b/plugins/octopus-deploy/package.json @@ -36,6 +36,7 @@ "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -53,7 +54,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/opencost/api-report.md b/plugins/opencost/api-report.md index ee9026cbb7..079f394618 100644 --- a/plugins/opencost/api-report.md +++ b/plugins/opencost/api-report.md @@ -6,10 +6,11 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const OpenCostPage: () => JSX.Element; +export const OpenCostPage: () => JSX_2.Element; // @public (undocumented) export const openCostPlugin: BackstagePlugin< diff --git a/plugins/opencost/package.json b/plugins/opencost/package.json index af2a275a52..6a3ede32d7 100644 --- a/plugins/opencost/package.json +++ b/plugins/opencost/package.json @@ -31,6 +31,7 @@ "@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/pickers": "^3.3.10", "@material-ui/styles": "^4.11.5", + "@types/react": "^16.13.1 || ^17.0.0", "axios": "^1.4.0", "date-fns": "^2.30.0", "lodash": "^4.17.21", diff --git a/plugins/org-react/api-report.md b/plugins/org-react/api-report.md index 48bd090f64..5cfe5ca90a 100644 --- a/plugins/org-react/api-report.md +++ b/plugins/org-react/api-report.md @@ -3,12 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// - import { GroupEntity } from '@backstage/catalog-model'; +import { default as React_2 } from 'react'; // @public (undocumented) -export const GroupListPicker: (props: GroupListPickerProps) => JSX.Element; +export const GroupListPicker: ( + props: GroupListPickerProps, +) => React_2.JSX.Element; // @public export type GroupListPickerProps = { diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index ce49dd71d0..d667c22151 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -40,6 +40,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -57,7 +58,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md index 7f21bfbb6b..704401e8d4 100644 --- a/plugins/org/api-report.md +++ b/plugins/org/api-report.md @@ -9,19 +9,21 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; // @public (undocumented) export const EntityGroupProfileCard: (props: { variant?: InfoCardVariants | undefined; showLinks?: boolean | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityMembersListCard: (props: { memberDisplayTitle?: string | undefined; pageSize?: number | undefined; showAggregateMembersToggle?: boolean | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityOwnershipCard: (props: { @@ -30,26 +32,26 @@ export const EntityOwnershipCard: (props: { hideRelationsToggle?: boolean | undefined; relationsType?: string | undefined; entityLimit?: number | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const EntityUserProfileCard: (props: { variant?: InfoCardVariants | undefined; showLinks?: boolean | undefined; -}) => JSX.Element; +}) => JSX_2.Element; // @public (undocumented) export const GroupProfileCard: (props: { variant?: InfoCardVariants; showLinks?: boolean; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const MembersListCard: (props: { memberDisplayTitle?: string; pageSize?: number; showAggregateMembersToggle?: boolean; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public export const MyGroupsSidebarItem: (props: { @@ -57,7 +59,7 @@ export const MyGroupsSidebarItem: (props: { pluralTitle: string; icon: IconComponent; filter?: Record; -}) => JSX.Element | null; +}) => React_2.JSX.Element | null; // @public (undocumented) const orgPlugin: BackstagePlugin< @@ -77,11 +79,11 @@ export const OwnershipCard: (props: { hideRelationsToggle?: boolean; relationsType?: string; entityLimit?: number; -}) => JSX.Element; +}) => React_2.JSX.Element; // @public (undocumented) export const UserProfileCard: (props: { variant?: InfoCardVariants; showLinks?: boolean; -}) => JSX.Element; +}) => React_2.JSX.Element; ``` diff --git a/plugins/org/package.json b/plugins/org/package.json index 48d53ca521..87848bb675 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "p-limit": "^3.1.0", "pluralize": "^8.0.0", "qs": "^6.10.1", @@ -61,7 +62,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index f398a6bee7..24c2e103cf 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -12,12 +12,14 @@ import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { ReactNode } from 'react'; // @public (undocumented) export const EntityPagerDutyCard: ( props: EntityPagerDutyCardProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type EntityPagerDutyCardProps = { @@ -28,7 +30,7 @@ export type EntityPagerDutyCardProps = { // @public (undocumented) export const HomePagePagerDutyCard: ( props: CardExtensionProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type HomePagePagerDutyCardProps = PagerDutyEntity & { @@ -67,7 +69,7 @@ export type PagerDutyAssignee = { }; // @public @deprecated (undocumented) -export const PagerDutyCard: (props: EntityPagerDutyCardProps) => JSX.Element; +export const PagerDutyCard: (props: EntityPagerDutyCardProps) => JSX_2.Element; // @public @deprecated (undocumented) export type PagerDutyCardProps = EntityPagerDutyCardProps; @@ -215,7 +217,9 @@ export type PagerDutyUser = { }; // @public (undocumented) -export function TriggerButton(props: { children?: ReactNode }): JSX.Element; +export function TriggerButton(props: { + children?: ReactNode; +}): React_2.JSX.Element; // @public (undocumented) export class UnauthorizedError extends Error {} diff --git a/plugins/periskop/api-report.md b/plugins/periskop/api-report.md index 3332dd9edb..578fb1dc49 100644 --- a/plugins/periskop/api-report.md +++ b/plugins/periskop/api-report.md @@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { ConfigApi } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; // @public (undocumented) export interface AggregatedError { @@ -24,7 +25,7 @@ export interface AggregatedError { } // @public (undocumented) -export const EntityPeriskopErrorsCard: () => JSX.Element; +export const EntityPeriskopErrorsCard: () => JSX_2.Element; // @public (undocumented) interface Error_2 { diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index 7f044bc15f..38354ac646 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -37,6 +37,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "luxon": "^3.0.0", "react-use": "^17.2.4" }, @@ -56,7 +57,6 @@ "@testing-library/user-event": "^14.0.0", "@types/luxon": "^3.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0" }, "files": [ diff --git a/plugins/permission-react/api-report.md b/plugins/permission-react/api-report.md index 293343b7ac..3c1961c92c 100644 --- a/plugins/permission-react/api-report.md +++ b/plugins/permission-react/api-report.md @@ -12,6 +12,7 @@ import { EvaluatePermissionRequest } from '@backstage/plugin-permission-common'; import { EvaluatePermissionResponse } from '@backstage/plugin-permission-common'; import { IdentityApi } from '@backstage/core-plugin-api'; import { Permission } from '@backstage/plugin-permission-common'; +import { default as React_2 } from 'react'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { ResourcePermission } from '@backstage/plugin-permission-common'; @@ -65,7 +66,7 @@ export const PermissionedRoute: ( resourceRef: string | undefined; } ), -) => JSX.Element; +) => React_2.JSX.Element; // @public export function RequirePermission( diff --git a/plugins/playlist/api-report.md b/plugins/playlist/api-report.md index 7a732a4a53..cf0ebe8a37 100644 --- a/plugins/playlist/api-report.md +++ b/plugins/playlist/api-report.md @@ -10,6 +10,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { FetchApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; import { Playlist } from '@backstage/plugin-playlist-common'; import { PlaylistMetadata } from '@backstage/plugin-playlist-common'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -17,7 +18,7 @@ import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) export const EntityPlaylistDialog: ( props: EntityPlaylistDialogProps, -) => JSX.Element; +) => JSX_2.Element; // @public (undocumented) export type EntityPlaylistDialogProps = { @@ -93,7 +94,7 @@ export class PlaylistClient implements PlaylistApi { } // @public (undocumented) -export const PlaylistIndexPage: () => JSX.Element; +export const PlaylistIndexPage: () => JSX_2.Element; // @public (undocumented) export const playlistPlugin: BackstagePlugin< diff --git a/plugins/playlist/package.json b/plugins/playlist/package.json index 574aa712ab..88c3dd4f5d 100644 --- a/plugins/playlist/package.json +++ b/plugins/playlist/package.json @@ -42,6 +42,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "lodash": "^4.17.21", "pluralize": "^8.0.0", "qs": "^6.9.4", @@ -64,7 +65,6 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.0", "swr": "^2.0.0" }, diff --git a/plugins/puppetdb/api-report.md b/plugins/puppetdb/api-report.md index 48a3276460..c7d1d0ff0e 100644 --- a/plugins/puppetdb/api-report.md +++ b/plugins/puppetdb/api-report.md @@ -7,7 +7,9 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { JSX as JSX_2 } from 'react'; import { PathParams } from '@backstage/core-plugin-api'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SubRouteRef } from '@backstage/core-plugin-api'; @@ -15,7 +17,7 @@ import { SubRouteRef } from '@backstage/core-plugin-api'; export const isPuppetDbAvailable: (entity: Entity) => boolean; // @public -export const PuppetDbPage: () => JSX.Element; +export const PuppetDbPage: () => JSX_2.Element; // @public const puppetdbPlugin: BackstagePlugin<{}, {}, {}>; @@ -29,7 +31,7 @@ export const puppetDbReportRouteRef: SubRouteRef>; export const puppetDbRouteRef: RouteRef; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/puppetdb/package.json b/plugins/puppetdb/package.json index 5973e91ee5..abed02c845 100644 --- a/plugins/puppetdb/package.json +++ b/plugins/puppetdb/package.json @@ -43,6 +43,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.57", + "@types/react": "^16.13.1 || ^17.0.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -59,7 +60,6 @@ "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^14.0.0", "@types/node": "*", - "@types/react": "^16.13.1 || ^17.0.0", "msw": "^1.0.1" }, "files": [ diff --git a/plugins/rollbar/api-report.md b/plugins/rollbar/api-report.md index 5ee8a50ce3..45c672e7bb 100644 --- a/plugins/rollbar/api-report.md +++ b/plugins/rollbar/api-report.md @@ -10,13 +10,15 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; +import { JSX as JSX_2 } from 'react'; +import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; // @public (undocumented) -export const EntityPageRollbar: () => JSX.Element; +export const EntityPageRollbar: () => React_2.JSX.Element; // @public (undocumented) -export const EntityRollbarContent: () => JSX.Element; +export const EntityRollbarContent: () => JSX_2.Element; // @public (undocumented) const isPluginApplicableToEntity: (entity: Entity) => boolean; @@ -238,5 +240,5 @@ export type RollbarTopActiveItem = { }; // @public (undocumented) -export const Router: () => JSX.Element; +export const Router: () => React_2.JSX.Element; ``` diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 7b2c5ce5bb..3db6a4aab5 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -41,6 +41,7 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", "lodash": "^4.17.21", "react-sparklines": "^1.7.0", "react-use": "^17.2.4" @@ -61,7 +62,6 @@ "@testing-library/react-hooks": "^8.0.0", "@testing-library/user-event": "^14.0.0", "@types/node": "^16.11.26", - "@types/react": "^16.13.1 || ^17.0.0", "cross-fetch": "^3.1.5", "msw": "^1.0.0" }, diff --git a/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts b/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts index 3359bd1dba..81d3dc3e93 100644 --- a/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts +++ b/plugins/scaffolder-backend/src/modules/catalogModuleTemplateKind.ts @@ -24,8 +24,8 @@ import { ScaffolderEntitiesProcessor } from '../processor'; * @alpha */ export const catalogModuleTemplateKind = createBackendModule({ - moduleId: 'scaffolder', - pluginId: 'templateKind', + moduleId: 'templateKind', + pluginId: 'catalog', register(env) { env.registerInit({ deps: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts index 9f68eb848a..9b7673ea29 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.test.ts @@ -42,7 +42,10 @@ describe('publish:azure', () => { const config = new ConfigReader({ integrations: { azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, { host: 'myazurehostnotoken.com' }, ], }, @@ -115,7 +118,9 @@ describe('publish:azure', () => { 'myazurehostnotoken.com?repo=bob&owner=owner&organization=org', }, }), - ).rejects.toThrow(/No token provided for Azure Integration/); + ).rejects.toThrow( + /No credentials provided https:\/\/myazurehostnotoken.com\/org, please check your integrations config/, + ); }); it('should throw when no repo is returned', async () => { @@ -260,7 +265,10 @@ describe('publish:azure', () => { const customAuthorConfig = new ConfigReader({ integrations: { azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, { host: 'myazurehostnotoken.com' }, ], }, @@ -301,7 +309,10 @@ describe('publish:azure', () => { const customAuthorConfig = new ConfigReader({ integrations: { azure: [ - { host: 'dev.azure.com', token: 'tokenlols' }, + { + host: 'dev.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, { host: 'myazurehostnotoken.com' }, ], }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts index 96b7c87204..b6c0226163 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -15,10 +15,17 @@ */ import { InputError } from '@backstage/errors'; -import { ScmIntegrationRegistry } from '@backstage/integration'; +import { + DefaultAzureDevOpsCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; import { initRepoAndPush } from '../helpers'; import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces'; -import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api'; +import { + getBearerHandler, + getPersonalAccessTokenHandler, + WebApi, +} from 'azure-devops-node-api'; import { getRepoSourceDirectory, parseRepoUrl } from './util'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { Config } from '@backstage/config'; @@ -135,22 +142,23 @@ export function createPublishAzureAction(options: { ); } - const integrationConfig = integrations.azure.byHost(host); + const url = `https://${host}/${organization}`; + const credentialProvider = + DefaultAzureDevOpsCredentialsProvider.fromIntegrations(integrations); + const credentials = await credentialProvider.getCredentials({ url: url }); - if (!integrationConfig) { + if (credentials === undefined && ctx.input.token === undefined) { throw new InputError( - `No matching integration configuration for host ${host}, please check your integrations config`, + `No credentials provided ${url}, please check your integrations config`, ); } - if (!integrationConfig.config.token && !ctx.input.token) { - throw new InputError(`No token provided for Azure Integration ${host}`); - } + const authHandler = + ctx.input.token || credentials?.type === 'pat' + ? getPersonalAccessTokenHandler(ctx.input.token ?? credentials!.token) + : getBearerHandler(credentials!.token); - const token = ctx.input.token ?? integrationConfig.config.token!; - const authHandler = getPersonalAccessTokenHandler(token); - - const webApi = new WebApi(`https://${host}/${organization}`, authHandler); + const webApi = new WebApi(url, authHandler); const client = await webApi.getGitApi(); const createOptions: GitRepositoryCreateOptions = { name: repo }; const returnedRepo = await client.createRepository(createOptions, owner); @@ -187,14 +195,19 @@ export function createPublishAzureAction(options: { : config.getOptionalString('scaffolder.defaultAuthor.email'), }; + const auth = + ctx.input.token || credentials?.type === 'pat' + ? { + username: 'notempty', + password: ctx.input.token ?? credentials!.token, + } + : { token: credentials!.token }; + const commitResult = await initRepoAndPush({ dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), remoteUrl, defaultBranch, - auth: { - username: 'notempty', - password: token, - }, + auth: auth, logger: ctx.logger, commitMessage: gitCommitMessage ? gitCommitMessage diff --git a/plugins/scaffolder-react/alpha-api-report.md b/plugins/scaffolder-react/alpha-api-report.md index 58a3c8e208..11bb2c10c4 100644 --- a/plugins/scaffolder-react/alpha-api-report.md +++ b/plugins/scaffolder-react/alpha-api-report.md @@ -44,10 +44,10 @@ export function createNextScaffolderFieldExtension< // @alpha export const DefaultTemplateOutputs: (props: { output?: ScaffolderTaskOutput; -}) => JSX.Element | null; +}) => React_2.JSX.Element | null; // @alpha (undocumented) -export const EmbeddableWorkflow: (props: WorkflowProps) => JSX.Element; +export const EmbeddableWorkflow: (props: WorkflowProps) => React_2.JSX.Element; // @alpha export const extractSchemaFromStep: (inputStep: JsonObject) => { @@ -56,7 +56,9 @@ export const extractSchemaFromStep: (inputStep: JsonObject) => { }; // @alpha -export const Form: (props: PropsWithChildren) => JSX.Element; +export const Form: ( + props: PropsWithChildren, +) => React_2.JSX.Element; // @alpha export type FormProps = Pick< @@ -123,7 +125,7 @@ export interface ParsedTemplateSchema { } // @alpha -export const ReviewState: (props: ReviewStateProps) => JSX.Element; +export const ReviewState: (props: ReviewStateProps) => React_2.JSX.Element; // @alpha export type ReviewStateProps = { @@ -134,7 +136,7 @@ export type ReviewStateProps = { // @alpha export const ScaffolderField: ( props: PropsWithChildren, -) => JSX.Element; +) => React_2.JSX.Element; // @alpha export interface ScaffolderFieldProps { @@ -159,7 +161,7 @@ export interface ScaffolderFieldProps { // @alpha (undocumented) export function ScaffolderPageContextMenu( props: ScaffolderPageContextMenuProps, -): JSX.Element | null; +): React_2.JSX.Element | null; // @alpha (undocumented) export type ScaffolderPageContextMenuProps = { @@ -169,7 +171,7 @@ export type ScaffolderPageContextMenuProps = { }; // @alpha -export const Stepper: (stepperProps: StepperProps) => JSX.Element; +export const Stepper: (stepperProps: StepperProps) => React_2.JSX.Element; // @alpha export type StepperProps = { @@ -192,10 +194,10 @@ export const TaskLogStream: (props: { logs: { [k: string]: string[]; }; -}) => JSX.Element; +}) => React_2.JSX.Element; // @alpha -export const TaskSteps: (props: TaskStepsProps) => JSX.Element; +export const TaskSteps: (props: TaskStepsProps) => React_2.JSX.Element; // @alpha export interface TaskStepsProps { @@ -210,7 +212,7 @@ export interface TaskStepsProps { } // @alpha -export const TemplateCard: (props: TemplateCardProps) => JSX.Element; +export const TemplateCard: (props: TemplateCardProps) => React_2.JSX.Element; // @alpha export interface TemplateCardProps { @@ -227,10 +229,12 @@ export interface TemplateCardProps { } // @alpha -export const TemplateCategoryPicker: () => JSX.Element | null; +export const TemplateCategoryPicker: () => React_2.JSX.Element | null; // @alpha -export const TemplateGroup: (props: TemplateGroupProps) => JSX.Element | null; +export const TemplateGroup: ( + props: TemplateGroupProps, +) => React_2.JSX.Element | null; // @alpha (undocumented) export type TemplateGroupFilter = { @@ -260,7 +264,9 @@ export interface TemplateGroupProps { } // @alpha (undocumented) -export const TemplateGroups: (props: TemplateGroupsProps) => JSX.Element | null; +export const TemplateGroups: ( + props: TemplateGroupsProps, +) => React_2.JSX.Element | null; // @alpha (undocumented) export interface TemplateGroupsProps { diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index c618050810..23204695a4 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -295,7 +295,7 @@ export interface ScaffolderUseTemplateSecrets { // @public export const SecretsContextProvider: ( props: PropsWithChildren<{}>, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type TaskStream = { diff --git a/plugins/scaffolder/alpha-api-report.md b/plugins/scaffolder/alpha-api-report.md index de657c2138..8cacf6105e 100644 --- a/plugins/scaffolder/alpha-api-report.md +++ b/plugins/scaffolder/alpha-api-report.md @@ -7,6 +7,7 @@ import { FormProps as FormProps_2 } from '@backstage/plugin-scaffolder-react/alpha'; import type { FormProps as FormProps_3 } from '@rjsf/core-v5'; +import { JSX as JSX_2 } from 'react'; import { LayoutOptions } from '@backstage/plugin-scaffolder-react'; import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha'; import { PropsWithChildren } from 'react'; @@ -47,7 +48,7 @@ export type NextRouterProps = { // @alpha export const NextScaffolderPage: ( props: PropsWithChildren, -) => JSX.Element; +) => JSX_2.Element; // @alpha (undocumented) export type TemplateListPageProps = { diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 48f24868de..59d2d54fcc 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -23,6 +23,7 @@ import { FieldExtensionOptions as FieldExtensionOptions_2 } from '@backstage/plu import { FieldValidation } from '@rjsf/core'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; +import { JSX as JSX_2 } from 'react'; import { LayoutOptions as LayoutOptions_2 } from '@backstage/plugin-scaffolder-react'; import { LayoutTemplate as LayoutTemplate_2 } from '@backstage/plugin-scaffolder-react'; import { ListActionsResponse as ListActionsResponse_2 } from '@backstage/plugin-scaffolder-react'; @@ -500,7 +501,7 @@ export const ScaffolderLayouts: ComponentType<{ export type ScaffolderOutputlink = ScaffolderOutputLink; // @public -export const ScaffolderPage: (props: RouterProps) => JSX.Element; +export const ScaffolderPage: (props: RouterProps) => JSX_2.Element; // @public export const scaffolderPlugin: BackstagePlugin< @@ -550,7 +551,7 @@ export type ScaffolderTaskStatus = ScaffolderTaskStatus_2; export type ScaffolderUseTemplateSecrets = ScaffolderUseTemplateSecrets_2; // @public -export const TaskPage: (props: TaskPageProps) => JSX.Element; +export const TaskPage: (props: TaskPageProps) => React_2.JSX.Element; // @public export type TaskPageProps = { @@ -561,7 +562,7 @@ export type TaskPageProps = { export type TemplateParameterSchema = TemplateParameterSchema_2; // @public -export const TemplateTypePicker: () => JSX.Element | null; +export const TemplateTypePicker: () => React_2.JSX.Element | null; // @public @deprecated (undocumented) export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets_2; diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index 8afde8c4a5..97c8300c14 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -31,10 +31,12 @@ import { TypographyProps } from '@material-ui/core'; // @public (undocumented) export const AutocompleteFilter: ( props: SearchAutocompleteFilterProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public (undocumented) -export const CheckboxFilter: (props: SearchFilterComponentProps) => JSX.Element; +export const CheckboxFilter: ( + props: SearchFilterComponentProps, +) => React_2.JSX.Element; // @public export const createSearchResultListItemExtension: < @@ -46,7 +48,7 @@ export const createSearchResultListItemExtension: < // @public (undocumented) export const DefaultResultListItem: ( props: DefaultResultListItemProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type DefaultResultListItemProps = { @@ -62,7 +64,7 @@ export type DefaultResultListItemProps = { // @public (undocumented) export const HighlightedSearchResultText: ( props: HighlightedSearchResultTextProps, -) => JSX.Element; +) => React_2.JSX.Element; // @public export type HighlightedSearchResultTextProps = { @@ -100,7 +102,7 @@ export type SearchAutocompleteComponent =
+ + + +