diff --git a/.changeset/beige-feet-tap.md b/.changeset/beige-feet-tap.md new file mode 100644 index 0000000000..b37d249700 --- /dev/null +++ b/.changeset/beige-feet-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Do not use cross-fetch in the backend diff --git a/.changeset/blue-jobs-know.md b/.changeset/blue-jobs-know.md new file mode 100644 index 0000000000..35feed552b --- /dev/null +++ b/.changeset/blue-jobs-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `versions:bump --release next` command is updated to compare the `main` and `next` release manifests and prefer the latest. diff --git a/.changeset/brave-singers-burn.md b/.changeset/brave-singers-burn.md new file mode 100644 index 0000000000..e8ccfb42e7 --- /dev/null +++ b/.changeset/brave-singers-burn.md @@ -0,0 +1,13 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +The following processors now properly accept an `ScmIntegrationRegistry` (an +interface) instead of an `ScmIntegrations` (which is a concrete class). + +- `AzureDevOpsDiscoveryProcessor` +- `CodeOwnersProcessor` +- `GitLabDiscoveryProcessor` +- `GithubDiscoveryProcessor` +- `GithubMultiOrgReaderProcessor` +- `GithubOrgReaderProcessor` diff --git a/.changeset/bright-buttons-rescue.md b/.changeset/bright-buttons-rescue.md deleted file mode 100644 index 71a6b76fa2..0000000000 --- a/.changeset/bright-buttons-rescue.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -'@backstage/plugin-auth-backend': minor ---- - -**BREAKING** Added `tokenManager` as a required property for the auth-backend `createRouter` function. This dependency is used to issue server tokens that are used by the `CatalogIdentityClient` when looking up users and their group membership during authentication. - -These changes are **required** to `packages/backend/src/plugins/auth.ts`: - -```diff -export default async function createPlugin({ - logger, - database, - config, - discovery, -+ tokenManager, -}: PluginEnvironment): Promise { - return await createRouter({ - logger, - config, - database, - discovery, -+ tokenManager, - }); -} -``` - -**BREAKING** The `CatalogIdentityClient` constructor now expects a `TokenManager` instead of a `TokenIssuer`. The `TokenManager` interface is used to generate a server token when [resolving a user's identity and membership through the catalog](https://backstage.io/docs/auth/identity-resolver). Using server tokens for these requests allows the auth-backend to bypass authorization checks when permissions are enabled for Backstage. This change will break apps that rely on the user tokens that were previously used by the client. Refer to the ["Backend-to-backend Authentication" tutorial](https://backstage.io/docs/tutorials/backend-to-backend-auth) for more information on server token usage. diff --git a/.changeset/bright-games-eat.md b/.changeset/bright-games-eat.md new file mode 100644 index 0000000000..3bae70283f --- /dev/null +++ b/.changeset/bright-games-eat.md @@ -0,0 +1,24 @@ +--- +'@backstage/create-app': patch +--- + +Remove SearchContextProvider from `` + +The `SidebarSearchModal` exported from `plugin-search` internally renders `SearchContextProvider`, so it can be removed from `Root.tsx`: + +```diff +-import { +- SidebarSearchModal, +- SearchContextProvider, +-} from '@backstage/plugin-search'; ++import { SidebarSearchModal } from '@backstage/plugin-search'; + +... omitted ... + + } to="/search"> +- +- +- ++ + +``` diff --git a/.changeset/bright-nails-grin.md b/.changeset/bright-nails-grin.md new file mode 100644 index 0000000000..434a17efbb --- /dev/null +++ b/.changeset/bright-nails-grin.md @@ -0,0 +1,78 @@ +--- +'@backstage/backend-common': patch +'@backstage/catalog-client': patch +'@backstage/cli': patch +'@backstage/config-loader': patch +'@backstage/core-app-api': patch +'@backstage/core-components': patch +'@backstage/core-plugin-api': patch +'@backstage/errors': patch +'@backstage/integration': patch +'@backstage/integration-react': patch +'@backstage/release-manifests': patch +'@backstage/test-utils': patch +'@backstage/plugin-airbrake': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-apache-airflow': patch +'@backstage/plugin-api-docs': patch +'@backstage/plugin-auth-backend': patch +'@backstage/plugin-auth-node': patch +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-badges': patch +'@backstage/plugin-bazaar': patch +'@backstage/plugin-bitrise': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-graphql': patch +'@backstage/plugin-catalog-import': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-config-schema': patch +'@backstage/plugin-cost-insights': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-explore-react': patch +'@backstage/plugin-firehydrant': patch +'@backstage/plugin-fossa': patch +'@backstage/plugin-gcp-projects': patch +'@backstage/plugin-git-release-manager': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-gocd': patch +'@backstage/plugin-graphiql': patch +'@backstage/plugin-home': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-kafka': patch +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-newrelic': patch +'@backstage/plugin-newrelic-dashboard': patch +'@backstage/plugin-org': patch +'@backstage/plugin-pagerduty': patch +'@backstage/plugin-permission-backend': patch +'@backstage/plugin-permission-common': patch +'@backstage/plugin-permission-react': patch +'@backstage/plugin-rollbar': patch +'@backstage/plugin-rollbar-backend': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-sonarqube': patch +'@backstage/plugin-splunk-on-call': patch +'@backstage/plugin-tech-insights': patch +'@backstage/plugin-tech-radar': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-xcmetrics': patch +--- + +Bump `node-fetch` to version 2.6.7 and `cross-fetch` to version 3.1.5 diff --git a/.changeset/brown-impalas-greet.md b/.changeset/brown-impalas-greet.md new file mode 100644 index 0000000000..1717c33451 --- /dev/null +++ b/.changeset/brown-impalas-greet.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fix error handling of the `runCommand` helper to return `Error` +instance. diff --git a/.changeset/clever-pears-turn.md b/.changeset/clever-pears-turn.md new file mode 100644 index 0000000000..33e08c231b --- /dev/null +++ b/.changeset/clever-pears-turn.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-org': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-tech-insights-backend': patch +--- + +Use updated type names from `@backstage/catalog-client` diff --git a/.changeset/curvy-worms-exercise.md b/.changeset/curvy-worms-exercise.md new file mode 100644 index 0000000000..b88e9b5891 --- /dev/null +++ b/.changeset/curvy-worms-exercise.md @@ -0,0 +1,6 @@ +--- +'@backstage/catalog-model': patch +--- + +Deprecated `parseLocationReference` and `stringifyLocationReference`, +introducing `parseLocationRef` and `stringifyLocationRef` in their place. diff --git a/.changeset/cyan-oranges-sit.md b/.changeset/cyan-oranges-sit.md new file mode 100644 index 0000000000..8a3244745f --- /dev/null +++ b/.changeset/cyan-oranges-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Deprecates the `Location` type export from this package. Imports of the `Location` type should now be done from the `@backstage/catalog-client` package instead. diff --git a/.changeset/dependabot-8ed326b.md b/.changeset/dependabot-8ed326b.md new file mode 100644 index 0000000000..88a91baeac --- /dev/null +++ b/.changeset/dependabot-8ed326b.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +chore(deps-dev): bump `@elastic/elasticsearch-mock` from 0.3.0 to 1.0.0 diff --git a/.changeset/dependabot-f969614.md b/.changeset/dependabot-f969614.md deleted file mode 100644 index c3c8890ec9..0000000000 --- a/.changeset/dependabot-f969614.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -chore(deps): bump `prom-client` from 13.2.0 to 14.0.1 diff --git a/.changeset/dry-lies-move.md b/.changeset/dry-lies-move.md new file mode 100644 index 0000000000..b0eb1be6f7 --- /dev/null +++ b/.changeset/dry-lies-move.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-todo-backend': patch +--- + +Replace usage of `serializeEntityRef` with `stringifyEntityRef` diff --git a/.changeset/fast-ducks-sell.md b/.changeset/fast-ducks-sell.md new file mode 100644 index 0000000000..2ad654124d --- /dev/null +++ b/.changeset/fast-ducks-sell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Support "dependencyOf" relation in Resource entities diff --git a/.changeset/dependabot-9ec400d.md b/.changeset/fast-lizards-fly.md similarity index 50% rename from .changeset/dependabot-9ec400d.md rename to .changeset/fast-lizards-fly.md index 1fd5a85477..e6772b056f 100644 --- a/.changeset/dependabot-9ec400d.md +++ b/.changeset/fast-lizards-fly.md @@ -2,4 +2,4 @@ '@backstage/plugin-auth-backend': patch --- -chore(deps): bump `passport` from 0.4.1 to 0.5.2 +Bump `passport-oauth2` to version 1.6.1 diff --git a/.changeset/fast-stingrays-hope.md b/.changeset/fast-stingrays-hope.md new file mode 100644 index 0000000000..688f14c548 --- /dev/null +++ b/.changeset/fast-stingrays-hope.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog-backend-module-ldap': patch +--- + +Import `Location` from the `@backstage/catalog-client` package. diff --git a/.changeset/flat-cars-begin.md b/.changeset/flat-cars-begin.md deleted file mode 100644 index 3a8837dc0e..0000000000 --- a/.changeset/flat-cars-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Change subtitle of Header style to use palette.bursts.fontColor diff --git a/.changeset/flat-crabs-smash.md b/.changeset/flat-crabs-smash.md new file mode 100644 index 0000000000..b56b340605 --- /dev/null +++ b/.changeset/flat-crabs-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `versions:bump` command now filters out `npm_` environment configuration when running `yarn install`. This has the effect of allowing it to consider local configuration files within the repository, which is the behavior that one would expect. diff --git a/.changeset/gold-queens-clap.md b/.changeset/gold-queens-clap.md deleted file mode 100644 index 252fbb1626..0000000000 --- a/.changeset/gold-queens-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core-components': patch ---- - -Fix an issue where changes related to the `MobileSidebar` prevented scrolling pages. Additionally improve the menu of the `MobileSidebar` to not overlay the `BottomNavigation`. diff --git a/.changeset/green-bobcats-burn.md b/.changeset/green-bobcats-burn.md new file mode 100644 index 0000000000..c28f4da678 --- /dev/null +++ b/.changeset/green-bobcats-burn.md @@ -0,0 +1,22 @@ +--- +'@backstage/plugin-permission-backend': minor +--- + +Add a warning if the permission backend is used without setting `permission.enabled=true`. + +**BREAKING** Permission backend's `createRouter` now requires a `config` option. + +```diff +// packages/backend/src/plugins/permission.ts + +... +export default async function createPlugin({ + ... ++ config, +}: PluginEnvironment) { + return createRouter({ + ... ++ config, + }); +} +``` diff --git a/.changeset/happy-lemons-drop.md b/.changeset/happy-lemons-drop.md new file mode 100644 index 0000000000..e4c6f52409 --- /dev/null +++ b/.changeset/happy-lemons-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-common': minor +--- + +**BREAKING** `PermissionCriteria` now requires at least one condition in `anyOf` and `allOf` arrays. This addresses some ambiguous behavior outlined in #9280. diff --git a/.changeset/hip-carrots-suffer.md b/.changeset/hip-carrots-suffer.md new file mode 100644 index 0000000000..3823cf982f --- /dev/null +++ b/.changeset/hip-carrots-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Fix bug where tech docs collator stores search indices with wrong entity ref casing. Make the collator to conform legacyPathCasing configuration option. diff --git a/.changeset/honest-shrimps-clap.md b/.changeset/honest-shrimps-clap.md new file mode 100644 index 0000000000..12ae623704 --- /dev/null +++ b/.changeset/honest-shrimps-clap.md @@ -0,0 +1,9 @@ +--- +'@backstage/catalog-client': minor +--- + +**BREAKING**: Removed the explicit `DiscoveryApi` and `FetchApi` export symbols, +which were unnecessary duplicates from the well known core ones. + +The `CATALOG_FILTER_EXISTS` symbol's value has changed. However, this should not +affect any code in practice. diff --git a/.changeset/lazy-jeans-obey.md b/.changeset/lazy-jeans-obey.md new file mode 100644 index 0000000000..9124b983dc --- /dev/null +++ b/.changeset/lazy-jeans-obey.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Update internal `Location` validation. diff --git a/.changeset/little-ravens-kick.md b/.changeset/little-ravens-kick.md new file mode 100644 index 0000000000..1e14f2f37e --- /dev/null +++ b/.changeset/little-ravens-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `versions:bump` command now also considers the root `package.json` when searching for updates. It has also received updates to its output, including a link the [Backstage upgrade helper](https://backstage.github.io/upgrade-helper) and silenced `yarn install` output. diff --git a/.changeset/mighty-beds-fix.md b/.changeset/mighty-beds-fix.md new file mode 100644 index 0000000000..3e5ab735dd --- /dev/null +++ b/.changeset/mighty-beds-fix.md @@ -0,0 +1,11 @@ +--- +'@backstage/catalog-client': patch +'@backstage/techdocs-common': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-todo-backend': patch +--- + +Updated to use new names for `parseLocationRef` and `stringifyLocationRef` diff --git a/.changeset/moody-knives-jump.md b/.changeset/moody-knives-jump.md new file mode 100644 index 0000000000..84e80d9f84 --- /dev/null +++ b/.changeset/moody-knives-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cicd-statistics': patch +--- + +Updated `@backstage/core-plugin-api` dependency. diff --git a/.changeset/nasty-pets-glow.md b/.changeset/nasty-pets-glow.md deleted file mode 100644 index e630736261..0000000000 --- a/.changeset/nasty-pets-glow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Added `relations.memberof` filter to the catalog api call in `MemberListCard` to avoid fetching all the User entity kinds from catalog-backend. diff --git a/.changeset/olive-flies-reflect.md b/.changeset/olive-flies-reflect.md new file mode 100644 index 0000000000..80075d8fb0 --- /dev/null +++ b/.changeset/olive-flies-reflect.md @@ -0,0 +1,6 @@ +--- +'@backstage/catalog-model': minor +--- + +**BREAKING**: Remove deprecated validation methods `analyzeLocationSchema`, `locationSchema` and `locationSpecSchema`. +This functionality was primarily used internally by the `catalog-backend`. diff --git a/.changeset/perfect-papayas-crash.md b/.changeset/perfect-papayas-crash.md new file mode 100644 index 0000000000..99c93d1b8d --- /dev/null +++ b/.changeset/perfect-papayas-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': minor +--- + +**BREAKING** `PermissionCriteria` now requires at least one condition in `anyOf` and `allOf` arrays. This addresses some ambiguous behavior outlined in #9280. diff --git a/.changeset/pink-onions-ring.md b/.changeset/pink-onions-ring.md new file mode 100644 index 0000000000..2b81ad4b43 --- /dev/null +++ b/.changeset/pink-onions-ring.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +**BREAKING** Completely removed the `EntitySystemDiagramCard` component which was deprecated in a previous release. Any remaining references to this component are now broken and should be replaced with `EntityCatalogGraphCard`, which can be imported from package `@backstage/plugin-catalog-graph`. diff --git a/.changeset/plenty-boxes-wink.md b/.changeset/plenty-boxes-wink.md new file mode 100644 index 0000000000..0011641938 --- /dev/null +++ b/.changeset/plenty-boxes-wink.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +**BREAKING**: Remove deprecated `serializeEntityRef` which is replaced by `stringifyEntityRef`. diff --git a/.changeset/polite-dots-train.md b/.changeset/polite-dots-train.md new file mode 100644 index 0000000000..dbd11d3e60 --- /dev/null +++ b/.changeset/polite-dots-train.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-yeoman': patch +--- + +Bump `yeoman-environment` dependency from `^3.6.0` to `^3.9.1`. diff --git a/.changeset/purple-files-hang.md b/.changeset/purple-files-hang.md new file mode 100644 index 0000000000..680de46a80 --- /dev/null +++ b/.changeset/purple-files-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': patch +--- + +Export the `Location` type that was previously exported by the `@backstage/catalog-model` package. diff --git a/.changeset/seven-rocks-share.md b/.changeset/seven-rocks-share.md new file mode 100644 index 0000000000..db131a85dc --- /dev/null +++ b/.changeset/seven-rocks-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration-react': patch +--- + +Updated the `ScmAuth` error message for missing provider configurations to link to `ScmAuthApi` setup documentation. diff --git a/.changeset/sharp-dragons-divide.md b/.changeset/sharp-dragons-divide.md deleted file mode 100644 index 398d7fb38a..0000000000 --- a/.changeset/sharp-dragons-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@techdocs/cli': patch ---- - -Use a local file dependency for embedded-techdocs-app, to ensure that it's always pulled out of the workspace diff --git a/.changeset/silly-ghosts-dance.md b/.changeset/silly-ghosts-dance.md new file mode 100644 index 0000000000..e83fc87521 --- /dev/null +++ b/.changeset/silly-ghosts-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Improve overall appearance of highlighted code in docs. diff --git a/.changeset/sour-chairs-double.md b/.changeset/sour-chairs-double.md deleted file mode 100644 index 4f481e20e7..0000000000 --- a/.changeset/sour-chairs-double.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Added `tokenManager` as a required property for the auth-backend `createRouter` function. This dependency is used to issue server tokens that are used by the `CatalogIdentityClient` when looking up users and their group membership during authentication. - -These changes are **required** to `packages/backend/src/plugins/auth.ts`: - -```diff -export default async function createPlugin({ - logger, - database, - config, - discovery, -+ tokenManager, -}: PluginEnvironment): Promise { - return await createRouter({ - logger, - config, - database, - discovery, -+ tokenManager, - }); -} -``` diff --git a/.changeset/sour-hats-love.md b/.changeset/sour-hats-love.md new file mode 100644 index 0000000000..f2cc88e66a --- /dev/null +++ b/.changeset/sour-hats-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Updated `isDatabaseConflictError` to handle modern sqlite conflict errors diff --git a/.changeset/stupid-cows-design.md b/.changeset/stupid-cows-design.md new file mode 100644 index 0000000000..2df99664fb --- /dev/null +++ b/.changeset/stupid-cows-design.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Deprecates the following exports which all have very limited and internal use. `ENTITY_META_GENERATED_FIELDS`, `entityHasChanges`, `generateEntityEtag`, `generateEntityUid`, `generateUpdatedEntity`, `ENTITY_META_GENERATED_FIELDS`. diff --git a/.changeset/sweet-keys-dress.md b/.changeset/sweet-keys-dress.md new file mode 100644 index 0000000000..4217f2fcf4 --- /dev/null +++ b/.changeset/sweet-keys-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated the default [sucrase](https://github.com/alangpierce/sucrase)-based Jest transform to include source maps if the environment variable `ENABLE_SOURCE_MAPS` is non-empty. This can be used to better support editor test debugging integrations. diff --git a/.changeset/tall-rats-lie.md b/.changeset/tall-rats-lie.md deleted file mode 100644 index ed76970df4..0000000000 --- a/.changeset/tall-rats-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-pagerduty': patch ---- - -Fix change events tab error when change events exist diff --git a/.changeset/tame-ads-exercise.md b/.changeset/tame-ads-exercise.md deleted file mode 100644 index a2e11393a6..0000000000 --- a/.changeset/tame-ads-exercise.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -'@backstage/plugin-catalog-import': minor ---- - -Make filename, branch name and examples URLs used in catalog import customizable. - -Catalog backend ingestion loop can be already configured to fetch targets with custom catalog filename (other than `catalog-info.yaml`). It's now possible to customize said filename and branch name used in pull requests created by catalog import flow too. This allows organizations to further customize Backstage experience and to better reflect their branding. - -Filename (default: `catalog-info.yaml`) and branch name (default: `backstage-integration`) used in pull requests can be configured in `app-config.yaml` as follows: - -```yaml -// app-config.yaml - -catalog: - import: - entityFilename: anvil.yaml - pullRequestBranchName: anvil-integration -``` - -Following React components have also been updated to accept optional props for providing example entity and repository paths: - -```tsx - -``` - -```tsx - -``` diff --git a/.changeset/tasty-eagles-retire.md b/.changeset/tasty-eagles-retire.md new file mode 100644 index 0000000000..c39f30d57f --- /dev/null +++ b/.changeset/tasty-eagles-retire.md @@ -0,0 +1,11 @@ +--- +'@backstage/catalog-client': patch +--- + +Deprecated the following types used by the catalog client, and created new +corresponding types to make them more consistent: + +- `CatalogEntitiesRequest` -> `GetEntitiesRequest` +- `CatalogListResponse` was removed and generally replaced with `GetEntitiesResponse` (which does not use a type parameter argument) +- `CatalogEntityAncestorsRequest`-> `GetEntityAncestorsRequest` +- `CatalogEntityAncestorsResponse` -> `GetEntityAncestorsResponse` diff --git a/.changeset/tasty-taxis-dance.md b/.changeset/tasty-taxis-dance.md new file mode 100644 index 0000000000..34ed30f686 --- /dev/null +++ b/.changeset/tasty-taxis-dance.md @@ -0,0 +1,14 @@ +--- +'@backstage/catalog-client': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-backend-module-ldap': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-github-deployments': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-todo': patch +'@backstage/plugin-todo-backend': patch +--- + +Replaces use of deprecated catalog-model constants. diff --git a/.changeset/thirty-houses-juggle.md b/.changeset/thirty-houses-juggle.md deleted file mode 100644 index c54ab5526f..0000000000 --- a/.changeset/thirty-houses-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Pass authorization token to location service inside location api routes diff --git a/.changeset/tidy-boxes-sell.md b/.changeset/tidy-boxes-sell.md new file mode 100644 index 0000000000..82ee16af57 --- /dev/null +++ b/.changeset/tidy-boxes-sell.md @@ -0,0 +1,18 @@ +--- +'@backstage/plugin-search': minor +--- + +**BREAKING**: `useSearch` doesn't return anymore `open` and `toggleModal`. +The two properties have been moved to the `useSearchModal` hook. + +``` +import { SearchModal, useSearchModal } from '@backstage/plugin-search'; + +const Foo = () => { + const { state, setOpen, toggleModal } = useSearchModal(); + + return ( + + ); +}; +``` diff --git a/.changeset/tiny-buses-compete.md b/.changeset/tiny-buses-compete.md deleted file mode 100644 index 2cb8d1a87f..0000000000 --- a/.changeset/tiny-buses-compete.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/core-components': patch -'@backstage/plugin-scaffolder': patch ---- - -Make linkTarget configurable for MarkdownContent component diff --git a/.changeset/tough-stingrays-sit.md b/.changeset/tough-stingrays-sit.md new file mode 100644 index 0000000000..5cef08c8e3 --- /dev/null +++ b/.changeset/tough-stingrays-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Optimized entity provider mutations with large numbers of new additions, such as big initial startup commits diff --git a/.changeset/twenty-dodos-rule.md b/.changeset/twenty-dodos-rule.md new file mode 100644 index 0000000000..5167811a3d --- /dev/null +++ b/.changeset/twenty-dodos-rule.md @@ -0,0 +1,6 @@ +--- +'@backstage/catalog-model': patch +--- + +Deprecates `EDIT_URL_ANNOTATION`, `LOCATION_ANNOTATION`, `ORIGIN_LOCATION_ANNOTATION`, `SOURCE_LOCATION_ANNOTATION`, `VIEW_URL_ANNOTATION` +and replaces these constants all prefixed with ANNOTATION\_ `ANNOTATION_EDIT_URL`, `ANNOTATION_LOCATION`, `ANNOTATION_ORIGIN_LOCATION`, `ANNOTATION_SOURCE_LOCATION`, `ANNOTATION_VIEW_URL` diff --git a/.changeset/witty-lamps-laugh.md b/.changeset/witty-lamps-laugh.md deleted file mode 100644 index 1ca4f4e4dc..0000000000 --- a/.changeset/witty-lamps-laugh.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Switched the `app` dependency in the backend to use a file target rather than version. - -To apply this change to an existing app, make the following change to `packages/backend/package.json`: - -```diff - "dependencies": { -- "app": "0.0.0", -+ "app": "file:../app", -``` diff --git a/.changeset/witty-lizards-nail.md b/.changeset/witty-lizards-nail.md deleted file mode 100644 index 2ce3faa3f7..0000000000 --- a/.changeset/witty-lizards-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Log warning if unable to parse yarn.lock diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a587419d50..1a0f827fb1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,43 +4,50 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @backstage/reviewers -/docs/features/techdocs @backstage/techdocs-core -/docs/features/search @backstage/techdocs-core -/docs/assets/search @backstage/techdocs-core -/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps -/plugins/circleci @backstage/reviewers @adamdmharvey -/plugins/code-coverage @backstage/reviewers @alde @nissayeva -/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva -/plugins/cost-insights @backstage/silver-lining -/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios -/plugins/search @backstage/techdocs-core -/plugins/search-* @backstage/techdocs-core -/plugins/techdocs @backstage/techdocs-core -/plugins/techdocs-backend @backstage/techdocs-core -/plugins/ilert @backstage/reviewers @yacut -/plugins/home @backstage/techdocs-core -/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin -/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin -/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin -/plugins/jenkins @backstage/reviewers @timja -/plugins/jenkins-backend @backstage/reviewers @timja -/plugins/kafka @backstage/reviewers @nirga -/plugins/kafka-backend @backstage/reviewers @nirga -/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka -/plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski -/plugins/git-release-manager @backstage/reviewers @erikengervall -/tech-insights-backend @backstage/reviewers @xantier @iain-b -/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b -/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b -/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b -/packages/embedded-techdocs-app @backstage/techdocs-core -/packages/search-common @backstage/techdocs-core -/packages/techdocs-cli @backstage/techdocs-core -/packages/techdocs-common @backstage/techdocs-core -/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining -/.changeset/search-* @backstage/techdocs-core -/.changeset/techdocs-* @backstage/techdocs-core -/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core -/plugins/apache-airflow @backstage/reviewers @cmpadden -/plugins/newrelic-dashboard @backstage/reviewers @mufaddal7 +* @backstage/reviewers +/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining +/.changeset/search-* @backstage/reviewers @backstage/techdocs-core +/.changeset/techdocs-* @backstage/reviewers @backstage/techdocs-core +/cypress/src/integration/plugins/techdocs.spec.ts @backstage/reviewers @backstage/techdocs-core +/docs/assets/search @backstage/reviewers @backstage/techdocs-core +/docs/features/search @backstage/reviewers @backstage/techdocs-core +/docs/features/techdocs @backstage/reviewers @backstage/techdocs-core +/packages/search-common @backstage/reviewers @backstage/techdocs-core +/packages/techdocs-cli @backstage/reviewers @backstage/techdocs-core +/packages/techdocs-cli-embedded-app @backstage/reviewers @backstage/techdocs-core +/packages/techdocs-common @backstage/reviewers @backstage/techdocs-core +/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps +/plugins/apache-airflow @backstage/reviewers @cmpadden +/plugins/api-docs @backstage/reviewers @backstage/sda-se-reviewers +/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin +/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin +/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin +/plugins/bitrise @backstage/reviewers @backstage/sda-se-reviewers +/plugins/catalog-graph @backstage/reviewers @backstage/sda-se-reviewers +/plugins/circleci @backstage/reviewers @adamdmharvey +/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios +/plugins/code-coverage @backstage/reviewers @alde @nissayeva +/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva +/plugins/cost-insights @backstage/reviewers @backstage/silver-lining +/plugins/explore @backstage/reviewers @backstage/sda-se-reviewers +/plugins/explore-react @backstage/reviewers @backstage/sda-se-reviewers +/plugins/fossa @backstage/reviewers @backstage/sda-se-reviewers +/plugins/git-release-manager @backstage/reviewers @erikengervall +/plugins/home @backstage/reviewers @backstage/techdocs-core +/plugins/ilert @backstage/reviewers @yacut +/plugins/jenkins @backstage/reviewers @timja +/plugins/jenkins-backend @backstage/reviewers @timja +/plugins/kafka @backstage/reviewers @nirga +/plugins/kafka-backend @backstage/reviewers @nirga +/plugins/newrelic-dashboard @backstage/reviewers @mufaddal7 +/plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski +/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka +/plugins/search @backstage/reviewers @backstage/techdocs-core +/plugins/search-* @backstage/reviewers @backstage/techdocs-core +/plugins/sonarqube @backstage/reviewers @backstage/sda-se-reviewers +/plugins/techdocs @backstage/reviewers @backstage/techdocs-core +/plugins/techdocs-backend @backstage/reviewers @backstage/techdocs-core +/tech-insights-backend @backstage/reviewers @xantier @iain-b +/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b +/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b +/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md index 63ae5e373a..04f8fd68d0 100644 --- a/.github/ISSUE_TEMPLATE/bug_template.md +++ b/.github/ISSUE_TEMPLATE/bug_template.md @@ -4,20 +4,17 @@ about: 'Create Bug Report' labels: bug --- - + ## Expected Behavior -## Current Behavior +## Actual Behavior - - -## Possible Solution - - - + ## Steps to Reproduce @@ -38,8 +35,10 @@ labels: bug - +- Browser Information: -- NodeJS Version (v14): -- Operating System and Version (e.g. Ubuntu 14.04): -- Browser Information: +- Output of `yarn backstage-cli info`: + +```text + +``` diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..e9f23287b7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +--- +blank_issues_enabled: false +contact_links: + - about: 'Please ask and answer usage questions in GitHub Discussions' + name: Question + url: 'https://github.com/backstage/backstage/discussions' + - about: 'Alternatively, you can use the Backstage Community Discord' + name: Chat + url: 'https://discord.gg/MUpMjP2' + - about: 'Please check the FAQ before filing new issues' + name: 'Backstage FAQ' + url: 'https://backstage.io/docs/FAQ' diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 49c85e7e1c..8303510e77 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -28,6 +28,7 @@ Blackbox bool boolean builtins +callout Chai changeset changesets @@ -223,6 +224,7 @@ productional Protobuf proxying Proxying +pseudonymized pubsub pygments pymdownx @@ -314,6 +316,7 @@ unbreak Unconference unmanaged unregister +unregistering unregistration untracked upsert @@ -337,4 +340,4 @@ Zalando Zhou zoomable zsh -Alef \ No newline at end of file +Alef diff --git a/.github/workflows/automate_techdocs-project-board.yml b/.github/workflows/automate_techdocs-project-board.yml deleted file mode 100644 index f551412f95..0000000000 --- a/.github/workflows/automate_techdocs-project-board.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Automate TechDocs project board -# Development of TechDocs in Backstage is managed by this Kanban board - https://github.com/orgs/backstage/projects/1 -# New issues and PRs with TechDocs in their title or docs-like-code label will be added to the board. -# Caveat: New PRs created from forks will not be added since GitHub Actions don't share credentials with forks. - -on: - issues: - types: [opened, reopened, labeled, edited] - pull_request: - types: [opened, reopened, labeled, edited] - -jobs: - assign_issue_or_pr_to_project: - runs-on: ubuntu-latest - name: Triage - env: - MY_GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} - steps: - - name: Assign new issue to Incoming based on its title. - uses: srggrs/assign-one-project-github-action@1.2.0 - if: | - env.MY_GITHUB_TOKEN != null && - contains(github.event.issue.title, 'TechDocs') || - contains(github.event.issue.title, 'techdocs') || - contains(github.event.issue.title, 'Techdocs') - with: - project: 'https://github.com/orgs/backstage/projects/1' - column_name: 'Incoming' - - - name: Assign new issue to Incoming based on its label. - uses: srggrs/assign-one-project-github-action@1.2.0 - if: | - env.MY_GITHUB_TOKEN != null && - contains(github.event.issue.labels.*.name, 'docs-like-code') - with: - project: 'https://github.com/orgs/backstage/projects/1' - column_name: 'Incoming' - - - name: Assign new PR to Incoming based on its title. - uses: srggrs/assign-one-project-github-action@1.2.0 - if: | - env.MY_GITHUB_TOKEN != null && - contains(github.event.pull_request.title, 'TechDocs') || - contains(github.event.pull_request.title, 'techdocs') || - contains(github.event.pull_request.title, 'Techdocs') - with: - project: 'https://github.com/orgs/backstage/projects/1' - column_name: 'Incoming' - - - name: Assign new PR to Incoming based on its label. - uses: srggrs/assign-one-project-github-action@1.2.0 - if: | - env.MY_GITHUB_TOKEN != null && - contains(github.event.pull_request.labels.*.name, 'docs-like-code') - with: - project: 'https://github.com/orgs/backstage/projects/1' - column_name: 'Incoming' diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index c21b7b7106..ed02a313fe 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -3,7 +3,7 @@ name: Deploy Packages on: workflow_dispatch: push: - branches: [master, release-*-patch] + branches: [master, patch/*] jobs: build: @@ -123,6 +123,7 @@ jobs: bash <(curl -s https://codecov.io/bash) -f packages/core-components/coverage/* -F core-components bash <(curl -s https://codecov.io/bash) -f packages/core-plugin-api/coverage/* -F core-plugin-api env: + BACKSTAGE_TEST_DISABLE_DOCKER: 1 BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres13.ports[5432] }} BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING: postgresql://postgres:postgres@localhost:${{ job.services.postgres9.ports[5432] }} BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING: mysql://root:root@localhost:${{ job.services.mysql8.ports[3306] }}/ignored @@ -204,7 +205,7 @@ jobs: # Grabs the version in the root package.json and creates a tag on GitHub - name: Create a release tag id: create_tag - run: node scripts/create-release-tag.js + run: node scripts/create-release-tag.js --dispatch-workflows env: GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index 39e270af16..4b44caafba 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -48,3 +48,31 @@ jobs: git add . git commit -am "${{ github.event.inputs.version }}" git push + + - name: Dispatch update-helper update + uses: actions/github-script@v5 + with: + github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + # TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while + script: | + console.log('Dispatching upgrade helper sync - release version'); + await github.rest.actions.createWorkflowDispatch({ + owner: 'backstage', + repo: 'upgrade-helper-diff', + workflow_id: 'release.yml', + ref: 'master', + inputs: { + version: require('./backstage/package.json').version, + }, + }); + + console.log('Dispatching upgrade helper sync - create-app version'); + await github.rest.actions.createWorkflowDispatch({ + owner: 'backstage', + repo: 'upgrade-helper-diff', + workflow_id: 'release.yml', + ref: 'master', + inputs: { + version: require('./backstage/packages/create-app/package.json').version, + }, + }); diff --git a/.github/workflows/verify_dco.yaml b/.github/workflows/verify_dco.yaml new file mode 100644 index 0000000000..e2da767394 --- /dev/null +++ b/.github/workflows/verify_dco.yaml @@ -0,0 +1,65 @@ +name: Verify DCO +on: + schedule: + - cron: '*/15 * * * *' + +jobs: + dco-helper: + runs-on: ubuntu-latest + steps: + - name: Verify DCO status for open pull requests + uses: actions/github-script@v5 + with: + script: | + const owner = "backstage"; + const repo = "backstage"; + const pulls = await github.paginate(github.rest.pulls.list, { + state: "open", + owner, + repo, + }); + + for (const pull of pulls) { + // Pick out the PRs that have the DCO check + const checks = await github.rest.checks.listForRef({ + owner, + repo, + ref: pull.head.sha, + check_name: "DCO", + status: "completed", + }); + // Skip if there are no checks + if (!checks.data.check_runs.length) { + continue; + } + // Skip if the conclusion is not action_required + if (checks.data.check_runs[0].conclusion !== "action_required") { + console.log(`No checks found for PR #${pull.number}, skipping`); + continue; + } + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: pull.number, + }); + if (comments.find((c) => + c.user.login === "github-actions[bot]" && + c.body.includes("") + ) + ) { + console.log(`already commented on PR #${pull.number}, skipping`); + continue; + } + console.log(`creating comment on PR #${pull.number}`); + const body = ` + Thanks for the contribution! + All commits need to be DCO signed before merging. Please refer to the the [DCO section in CONTRIBUTING.md](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md#developer-certificate-of-origin) or the [DCO](${checks.data.check_runs[0].html_url}) status for more info. + `; + await github.rest.issues.createComment({ + repo, + owner, + issue_number: pull.number, + body, + }); + } diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 4098b44406..6694ace02b 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -78,3 +78,5 @@ jobs: run: | sudo sysctl fs.inotify.max_user_watches=524288 yarn e2e-test run + env: + BACKSTAGE_TEST_DISABLE_DOCKER: 1 diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index 7e9a7e85fa..3806eade25 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -44,3 +44,5 @@ jobs: - name: techdocs-cli e2e test working-directory: packages/techdocs-cli run: yarn test:e2e:ci + env: + BACKSTAGE_TEST_DISABLE_DOCKER: 1 diff --git a/ADOPTERS.md b/ADOPTERS.md index b95e20757d..1682e13316 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,87 +1,95 @@ -| Organization | Contact | Description of Use | -| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | -| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | -| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | -| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | -| [SDA SE](https://sda.se) | [@dschwank](https://github.com/dschwank), [@iammnils](https://github.com/iammnils) | Central place for developing and sharing services in our insurance ecosystem. | -| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | -| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | -| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | -| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | -| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | -| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | -| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | -| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | -| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | -| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | -| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | -| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | -| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | -| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | -| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | -| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | -| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | -| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | -| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | -| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | -| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | -| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | -| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | -| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | -| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | -| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | -| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | -| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | -| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | -| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | -| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | -| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | -| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | -| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | -| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | -| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | -| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | -| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | -| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | -| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | -| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | -| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | -| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | -| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | -| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. | -| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. | -| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems🚀. | -| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | -| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | -| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | -| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | -| [Adevinta](https://www.adevinta.com) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | -| [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation. | -| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. | -| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. | -| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | -| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem | -| [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services | -| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. | -| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. | -| [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! | -| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. | -| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 | -| [LogMeIn](https://www.logmein.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. | -| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates
Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc | -| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. | -| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon), [Gauthier Roebroeck](https://github.com/gauthier-roebroeck-mox) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 | -| [Keyloop](https://www.keyloop.com/) | [Andre Wanlin](https://github.com/awanlin) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 | -| [Simply Business](https://sbtech.simplybusiness.co.uk/) | [@addersuk](https://github.com/addersuk), [@LightningStairs](https://github.com/LightningStairs), [@punitcse](https://github.com/punitcse), [@moltenice](https://github.com/moltenice) | Central developer portal to access everything a developer needs such as docs, internal service catalog, and the ability to quickly create a new service from a template. Internally developed Backstage plugins allow us to customise the experience to how we work. | -| [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding | -| [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. | -| [EF Education First](https://www.ef.com) | [Daan Boerlage](https://github.com/runebaas), [Rafał Nowosielski](https://github.com/rnowosielski) | Our developer portal - primarily used for cataloging and scaffolding with the ambition to expand with more feature adoptions over time | -| [Power Home Remodeling](https://www.techatpower.com) | [Ben Langfeld](https://github.com/benlangfeld) | Developer portal to our internal services, build on open-source software (including Kubernetes) in our own datacenters. Our Portal allows our team members to navigate inherant complexity and standardise. | -| [Livspace](https://www.livspace.com) | [Praveen Kumar](https://github.com/praveen-livspace) | Developer portal, service catalog, tech docs, API docs and plugins | -| [Just Eat Takeaway](https://www.justeattakeaway.com) | [Kim Wilson](https://github.com/kwilson541) | Our developer portal which centralises applications, reduces cognitive load and provides teams insights. | -| [Hopin](https://hopin.com) | [Vladimir Glafirov](https://github.com/vglafirov), [Chloe Lee](https://github.com/msfuko) | Developer portal to streamline the development practices. Integrated with service catalog, software templates, application monitoring, tech docs and plugins. | -| [HBO Max](https://hbomax.com) | [@mdb](https://github.com/mdb), [@nesta219](https://github.com/nesta219), [@nmische](https://github.com/nmische), [@hbomark](https://github.com/hbomark) | Developer portal hosting service catalog and API documentation, as well as cloud infrastructure details, operational visibility tools, and a custom plugin for browsing notable platform change events, such as deployments and configuration updates. | +| Organization | Contact | Description of Use | +| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | +| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | +| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | +| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | +| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| [SDA SE](https://sda.se) | [@dschwank](https://github.com/dschwank), [@iammnils](https://github.com/iammnils) | Central place for developing and sharing services in our insurance ecosystem. | +| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | +| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | +| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | +| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | +| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | +| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | +| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | +| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | +| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | +| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | +| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | +| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | +| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | +| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | +| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | +| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | +| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | +| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | +| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | +| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | +| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | +| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | +| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | +| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process 🌕🚀🧑‍🚀 | +| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | +| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | +| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | +| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | +| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | +| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | +| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | +| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | +| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | +| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | +| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | +| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | +| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | +| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | +| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | +| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | +| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | +| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | +| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [Santagostino](https://santagostino.it) | [@santagostino](https://github.com/santagostino) | Developer portal, gateway to our infrastructure, documentation, service catalog and internal tooling. | +| [Peak](https://peak.ai) | [Luke Beamish](https://github.com/lukebeamish-peak) | Developer portal for all internal engineers to access documentation and tooling. | +| [Gelato](https://gelato.com/) | [Dmitry Makarenko](https://github.com/dmitry-makarenko-gelato) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal and third-party systems🚀. | +| [GoCardless](https://gocardless.com/) | [James Turley](https://github.com/tragiclifestories) | Developer portal: documentation, service templates, org structure, service catalog, plugins for integration with internal systems. | +| [Box](https://www.box.com) | [@kielosz](https://github.com/kielosz), [@jluk-box](https://github.com/jluk-box), [@ptychu](https://github.com/ptychu), [@alexrybch](https://github.com/alexrybch), [@szubster](https://github.com/szubster) | Developer portal for service catalog, integration with internal systems, new service onboarding. | +| [Bazaarvoice](https://www.bazaarvoice.com) | [@niallmccullagh](https://github.com/niallmccullagh) | Developer portal for service catalog and scaffolds, publishing Github docs and API documentation, visualising our internal tech radar and our product engineering org structure. | +| [Krateo PlatformOps](https://www.krateo.io) | [@projectkerberus](https://github.com/projectkerberus) | A multi-cloud control plane to create, manage and deploy any kind of resource easily and centrally via a Developer Portal that centralizes via a self-service catalog the templating and ownership of services, the available documentation, the overview of the components that compose an entire domain and all the data of the service lifecycle. | +| [Adevinta](https://www.adevinta.com) | [Ray Sinnema](https://github.com/RemonSinnema) | Showcase shared services to our internal customers. | +| [Splunk](https://www.splunk.com) | [@tonytamsf](https://github.com/tonytamsf) | Developer portal as a centralized place to find people, services, documentation, escalation policies and give bravos. This portal is also being used as a centralized search engine for engineering specific documentation. | +| [SoundCloud](https://www.soundcloud.com) | [Julio Zynger](https://github.com/julioz) | Developer portal as a [humane registry](https://martinfowler.com/bliki/HumaneRegistry.html) for the organization: catalog of people, services, documentation, feature toggles, escalation policies, etc. | +| [Volvofinans Bank](https://www.volvofinans.se) | [Johan Hammar](https://github.com/johanhammar) | Developer portal enabling engineers to manage and explore software and documentation. | +| [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | +| [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem | +| [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services | +| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. | +| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. | +| [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! | +| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. | +| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 | +| [GoTo](https://www.goto.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. | +| [Telstra](https://www.telstra.com.au) | [@kiranpatel11](https://github.com/kiranpatel11), [JasonC](https://github.com/JasonC17) | Primary usage: software catalog and templates
Emerging usage : TechDocs, Explore Ecosystem, TechRadar, etc | +| [Mosaico](https://www.mosaico.com.br/) | [Wédney Yuri](https://github.com/wedneyyuri),[@tino.milton](https://github.com/miltonjacomini) | A centralized service catalog of our documentation for our service engineers. | +| [Mox Bank](https://www.mox.com/) | [Nick Laqua](https://github.com/nick-laqua-dragon) | "Single pane of glass" developer portal for providing a best-in-class developer experience to our product teams and making Mox the best tech environment in Hongkong 🥰🚀 | +| [Keyloop](https://www.keyloop.com/) | [Andre Wanlin](https://github.com/awanlin) | Future-motive Developer Portal to help our teams create technology to make everything about buying and owning a car better. 🚗 | +| [Simply Business](https://sbtech.simplybusiness.co.uk/) | [@addersuk](https://github.com/addersuk), [@LightningStairs](https://github.com/LightningStairs), [@punitcse](https://github.com/punitcse), [@moltenice](https://github.com/moltenice) | Central developer portal to access everything a developer needs such as docs, internal service catalog, and the ability to quickly create a new service from a template. Internally developed Backstage plugins allow us to customise the experience to how we work. | +| [Overwolf](https://www.overwolf.com) | [@tomwolfgang](https://github.com/tomwolfgang) | Dev portal - software catalog, tech-docs, scaffolding | +| [Hotmart](https://www.hotmart.com) | [@fabioviana-hotmart](https://github.com/fabioviana-hotmart) | The main Developers Portal to centralize docs, applications and technical metrics. | +| [EF Education First](https://www.ef.com) | [Daan Boerlage](https://github.com/runebaas), [Rafał Nowosielski](https://github.com/rnowosielski) | Our developer portal - primarily used for cataloging and scaffolding with the ambition to expand with more feature adoptions over time | +| [Power Home Remodeling](https://www.techatpower.com) | [Ben Langfeld](https://github.com/benlangfeld) | Developer portal to our internal services, build on open-source software (including Kubernetes) in our own datacenters. Our Portal allows our team members to navigate inherant complexity and standardise. | +| [Livspace](https://www.livspace.com) | [Praveen Kumar](https://github.com/praveen-livspace) | Developer portal, service catalog, tech docs, API docs and plugins | +| [Just Eat Takeaway](https://www.justeattakeaway.com) | [Kim Wilson](https://github.com/kwilson541) | Our developer portal which centralises applications, reduces cognitive load and provides teams insights. | +| [Hopin](https://hopin.com) | [Vladimir Glafirov](https://github.com/vglafirov), [Chloe Lee](https://github.com/msfuko) | Developer portal to streamline the development practices. Integrated with service catalog, software templates, application monitoring, tech docs and plugins. | +| [HBO Max](https://hbomax.com) | [@mdb](https://github.com/mdb), [@nesta219](https://github.com/nesta219), [@nmische](https://github.com/nmische), [@hbomark](https://github.com/hbomark) | Developer portal hosting service catalog and API documentation, as well as cloud infrastructure details, operational visibility tools, and a custom plugin for browsing notable platform change events, such as deployments and configuration updates. | +| [RCHLO](https://www.riachuelo.com.br) & [MIDWAY](https://www.midway.com.br) | [@marcosborges](https://github.com/marcosborges), [@defaultbr](https://github.com/defaultbr) | Self-Service Platform | +| [HP Inc](https://www.hp.com) | [Damon Kaswell](https://github.com/dekoding) | DevEx engagement hub (dev portal: docs, standards, Q&A) and extensive assets catalog (APIs, services, code, data, etc.) for the pan-HP internal developer community. | +| [VMware](https://www.vmware.com) | [@mpriamo](https://github.com/mpriamo), [@krisapplegate](https://github.com/krisapplegate) | Part of [Tanzu Application Platform](https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/index.html) offering; internal developer portal | +| [Ualá](https://www.uala.com.ar/) | [Santiago Bernal](https://github.com/sabernal) | Initial work being done to centralize documentation for all our microservices and APIs, as well as scaffolding new services and tracking code quality | +| [IKEA IT AB](https://www.ingka.com) | [@bjornramberg](https://github.com/bjornramberg), [@supriyachitale](https://github.com/supriyachitale) | Supporting engineers at scale with self serve access and connecting the dots of our engineering platform and services, enabling product teams to move faster and go further, and unleashing innovation, reuse and co-creation across the organisation. | +| [Invitae](https://www.invitae.com/en) | [@ryan-hanchett](https://github.com/ryan-hanchett), [@gmandler42](https://github.com/gmandler42) | Centralized Developer Experience portal, putting all of our tooling behind a single pane of glass and creating a living service catalog. | +| [Fortum](https://www.fortum.com/) | [@brunoamaroalmeida](https://github.com/brunoamaroalmeida), [@dhaval-vithalani](https://github.com/dhaval-vithalani), [@sunilkumarmohanty](https://github.com/sunilkumarmohanty) | A central portal containing information about our applications, services, processes and other software assets to be used by Fortum software engineering community. | +| [Procore](https://www.procore.com/jobs/engineering) | [@shayon](https://github.com/Shayon), [@jamesdabbs-procore](https://github.com/jamesdabbs-procore) | Developer portal - centralized software catalog and templates to enable self serve and reduce cognitive load. | diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 6990f72c3f..7597dd437c 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,69 +1,3 @@ -# Code of Conduct +# Community Code of Conduct -This code of conduct outlines our expectations for participants within the **Spotify FOSS** community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community. - -Our open source community strives to: - -- **Be friendly and patient.** -- **Be welcoming**: We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. -- **Be considerate**: Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. -- **Be respectful**: Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. -- **Be careful in the words that we choose**: we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. -- **Try to understand why we disagree**: Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. - -## Definitions - -Harassment includes, but is not limited to: - -- Offensive comments related to gender, gender identity and expression, sexual orientation, disability, mental illness, neuro(a)typicality, physical appearance, body size, race, age, regional discrimination, political or religious affiliation -- Unwelcome comments regarding a person’s lifestyle choices and practices, including those related to food, health, parenting, drugs, and employment -- Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not correctly reflect a person's gender identity. You must address people by the name they give you when not addressing them by their username or handle -- Physical contact and simulated physical contact (e.g., textual descriptions like “_hug_” or “_backrub_”) without consent or after a request to stop -- Threats of violence, both physical and psychological -- Incitement of violence towards any individual, including encouraging a person to commit suicide or to engage in self-harm -- Deliberate intimidation -- Stalking or following -- Harassing photography or recording, including logging online activity for harassment purposes -- Sustained disruption of discussion -- Unwelcome sexual attention, including gratuitous or off-topic sexual images or behaviour -- Pattern of inappropriate social contact, such as requesting/assuming inappropriate levels of intimacy with others -- Continued one-on-one communication after requests to cease -- Deliberate “outing” of any aspect of a person’s identity without their consent except as necessary to protect others from intentional abuse -- Publication of non-harassing private communication - -Our open source community prioritizes marginalized people’s safety over privileged people’s comfort. We will not act on complaints regarding: - -- ‘Reverse’ -isms, including ‘reverse racism,’ ‘reverse sexism,’ and ‘cisphobia’ -- Reasonable communication of boundaries, such as “leave me alone,” “go away,” or “I’m not discussing this with you” -- Refusal to explain or debate social justice concepts -- Communicating in a ‘tone’ you don’t find congenial -- Criticizing racist, sexist, cissexist, or otherwise oppressive behavior or assumptions - -### Diversity Statement - -We encourage everyone to participate and are committed to building a community for all. Although we will fail at times, we seek to treat everyone both as fairly and equally as possible. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. - -Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected -characteristics above, including participants with disabilities. - -### Reporting Issues - -If you experience or witness unacceptable behavior—or have any other concerns—please report it by contacting us via **fossboard@spotify.com**. All reports will be handled with discretion. In your report please include: - -- Your contact information. -- Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please - include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. -- Any additional information that may be helpful. - -After filing a report, a representative will contact you personally, review the incident, follow up with any additional questions, and make a decision as to how to respond. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. If the complaint originates from a member of the response team, it will be handled by a different member of the response team. We will respect confidentiality requests for the purpose of protecting victims of abuse. - -### Attribution & Acknowledgements - -We all stand on the shoulders of giants across many open source communities. We'd like to thank the communities and projects that established code of conducts and diversity statements as our inspiration: - -- [Django](https://www.djangoproject.com/conduct/reporting/) -- [Python](https://www.python.org/community/diversity/) -- [Ubuntu](http://www.ubuntu.com/about/about-ubuntu/conduct) -- [Contributor Covenant](http://contributor-covenant.org/) -- [Geek Feminism](http://geekfeminism.org/about/code-of-conduct/) -- [Citizen Code of Conduct](http://citizencodeofconduct.org/) +Backstage follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 936c27c7cf..3bcef61021 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -36,6 +36,25 @@ To become a maintainer you need to demonstrate the following: If a maintainer is no longer interested or cannot perform the maintainer duties listed above, they should volunteer to be moved to emeritus status. In extreme cases this can also occur by a vote of the sponsors and maintainers per the voting process below. +# End User Sponsors + +## Role of a Backstage End User Sponsor + +- Provide support for Backstage by removing blockers, securing funding, providing advocacy, feedback, and ensuring project continuity and long term success. +- Assist Backstage maintainers in prioritizing upcoming roadmap items and planned work. +- Provide neutral mediation for any disputes that arise as part of the project. + +## Backstage End User Sponsor Membership + +The End User Sponsors group comprises at most 5 people. To be eligible for membership in the group, you or the company where you work you must: + +- Be responsible for and end user of a production Backstage deployment of non-trivial size +- Be active contributors to the open source project +- Be willing and able to attend regularly-scheduled End User Sponsor meetings +- Abide by [Backstage’s Code of Conduct](./CODE_OF_CONDUCT.md). + +Candidates for membership will be nominated by current Sponsor members or by Backstage maintainers. If there are more nominations than Sponsor seats remaining, existing sponsors shall vote on the candidates, and the candidates with the most votes will become Sponsors. Any ties will be broken by current Backstage sponsors. + # Reviewers The project also contains a team called [@backstage/reviewers](https://github.com/orgs/backstage/teams/reviewers). This is the team of people who are the fallback in [`CODEOWNERS`](./.github/CODEOWNERS). This team will typically contain the maintainers, and a small number of additional people who are permitted to approve and merge pull requests. The purpose of this group is to offload some of the review work from the maintainers, simplifying and speeding up the review process for contributors. diff --git a/app-config.yaml b/app-config.yaml index b84b5de2f2..b025064c74 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -24,7 +24,8 @@ app: backend: # Used for enabling authentication, secret is shared by all backend plugins - # See backend-to-backend-auth.md in the docs for information on the format + # See https://backstage.io/docs/tutorials/backend-to-backend-auth for + # information on the format # auth: # keys: # - secret: ${BACKEND_SECRET} diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index 25dd08e054..45ae38b8b7 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -15,7 +15,10 @@ import cookieParser from 'cookie-parser'; import { Request, Response, NextFunction } from 'express'; import { JWT } from 'jose'; import { URL } from 'url'; -import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { + IdentityClient, + getBearerTokenFromAuthorizationHeader, +} from '@backstage/plugin-auth-node'; // ... @@ -44,7 +47,7 @@ async function main() { // ... const discovery = SingleHostDiscovery.fromConfig(config); - const identity = new IdentityClient({ + const identity = IdentityClient.create({ discovery, issuer: await discovery.getExternalBaseUrl('auth'), }); @@ -58,7 +61,7 @@ async function main() { ) => { try { const token = - IdentityClient.getBearerToken(req.headers.authorization) || + getBearerTokenFromAuthorizationHeader(req.headers.authorization) || req.cookies['token']; req.user = await identity.authenticate(token); if (!req.headers.authorization) { @@ -80,7 +83,7 @@ async function main() { const apiRouter = Router(); apiRouter.use(cookieParser()); - // The auth route must be publically available as it is used during login + // The auth route must be publicly available as it is used during login apiRouter.use('/auth', await auth(authEnv)); // Add a simple endpoint to be used when setting a token cookie apiRouter.use('/cookie', authMiddleware, (_req, res) => { diff --git a/contrib/scripts/README.md b/contrib/scripts/README.md new file mode 100644 index 0000000000..8ed7e09749 --- /dev/null +++ b/contrib/scripts/README.md @@ -0,0 +1,3 @@ +# Scripts + +Here you will find a variety of script contributions. diff --git a/contrib/scripts/orphan-clean-up/OrphanCleanUp.ps1 b/contrib/scripts/orphan-clean-up/OrphanCleanUp.ps1 new file mode 100644 index 0000000000..1eaaa2ab2c --- /dev/null +++ b/contrib/scripts/orphan-clean-up/OrphanCleanUp.ps1 @@ -0,0 +1,22 @@ +<# +.DESCRIPTION +Cleanes up orphaned entities for the provided Backstage URL, defaults to the local backend +#> +param( + [string]$backstageUrl = "http://localhost:7007" +) + +$orphanApiUrl = "$backstageUrl/api/catalog/entities?filter=metadata.annotations.backstage.io/orphan=true" +$orphanDeleteApiUrl = "$backstageUrl/api/catalog/entities/by-uid" + +$orphans = Invoke-RestMethod -Method Get -Uri $orphanApiUrl + +Write-Host "" +Write-Host "Found $($orphans.length) orphaned entities" +Write-Host "" + +foreach($orphan in $orphans){ + Write-Host "Deleting orphan $($orphan.metadata.name) of kind $($orphan.kind)" + + Invoke-RestMethod -Method Delete -Uri "$orphanDeleteApiUrl/$($orphan.metadata.uid)" +} diff --git a/contrib/scripts/orphan-clean-up/README.md b/contrib/scripts/orphan-clean-up/README.md new file mode 100644 index 0000000000..a6a25a4e7d --- /dev/null +++ b/contrib/scripts/orphan-clean-up/README.md @@ -0,0 +1,21 @@ +# Orphan Clean Up + +## Overview + +The Orphan Clean Up script is a basic PowerShell script to delete orphaned entities in the catalog. This script also assumes that you do not have authentication setup for your Backstage API endpoints. + +_Warning:_ There is a risk of entities being orphaned (and being deleted by this script) in case of the location having problems and returning a 404 status code. This might lead to accidental deletion of entities until the processing loop has recreated the entity. + +## Requirements + +This script is PowerShell based so therefore needs to be ran in a PowerShell session. If you are not able to use PowerShell the script should give you a clear idea as to how to create it with another scripting language like Bash. + +## Usage + +Here's how to use the script: + +1. Download the script +2. Now start a PowerShell session +3. Next navigate to the location you downloaded the script +4. Then run this command replacing `https:\\backstage.my-company.com` with the URL of your Backstage instance: `.\OrphanCleanUp.ps1 https:\\backstage.my-company.com` +5. The script will output the number of orphaned entities it finds and then the name for each one it deletes diff --git a/docs/auth/index.md b/docs/auth/index.md index e407480f65..6bda35232a 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -56,18 +56,15 @@ allows a single auth backend to serve multiple environments, such as running a local frontend against a deployed backend. The provider configuration matching the local `auth.environment` setting will be selected. -### Adding the provider to the sign-in page +## Using an authentication provider for sign-in -After configuring an authentication provider, the `app` frontend package needs a -small update to show this provider as a login option. The `SignInPage` component -handles this, and takes either a `provider` or `providers` (array) prop of -`SignInProviderConfig` definitions. +If you want to use an authentication provider for sign-in, as opposed to just accessing external resources, you'll need to configure that in your app as well. This is done by providing a custom `SignInPage` component to the app, which will require the user to sign in before they can access the app. Note that this does not block access to the app, which you can read more about [here](./using-auth.md). -These reference the `ApiRef` exported by the provider. Again, an example using -GitHub that can be adapted to any of the built-in providers: +If you want to, you can use the `SignInPage` component that is provided by `@backstage/core-components`, which takes either a `provider` or `providers` (array) prop of `SignInProviderConfig` definitions. These reference the `ApiRef` exported for the provider. + +Again, the following example for GitHub shows the additions needed to `packages/app/src/App.tsx`, and can be adapted to any of the built-in providers: ```diff -# packages/app/src/App.tsx + import { githubAuthApiRef } from '@backstage/core-plugin-api'; + import { SignInProviderConfig, SignInPage } from '@backstage/core-components'; @@ -78,8 +75,8 @@ GitHub that can be adapted to any of the built-in providers: + apiRef: githubAuthApiRef, +}; + -const app = createApp({ - apis, + const app = createApp({ + apis, + components: { + SignInPage: props => ( + + ), + }, - bindRoutes({ bind }) { + bindRoutes({ bind }) { ``` To also allow unauthenticated guest access, use the `providers` prop for `SignInPage`: ```diff -const app = createApp({ - apis, + const app = createApp({ + apis, + components: { + SignInPage: props => ( + + ), + }, - bindRoutes({ bind }) { + bindRoutes({ bind }) { ``` ## Adding a custom authentication provider @@ -119,3 +116,26 @@ Backstage uses [Passport](http://www.passportjs.org/) under the hood, which has a wide library of authentication strategies for different providers. See [Add authentication provider](add-auth-provider.md) for details on adding a new Passport-supported authentication method. + +## Custom ScmAuthApi Implementation + +If you are using any custom authentication providers, like for example one for GitHub Enterprise, then you are likely to need a custom implementation of the [`ScmAuthApi`](https://backstage.io/docs/reference/integration-react.scmauthapi). It is an API used to authenticate towards different SCM systems in a generic way, based on what resource is being accessed, and is used for example by the Scaffolder (Software Templates) and Catalog Import plugins. + +To set up a custom `ScmAuthApi` implementation, you'll need to add an API factory entry to `packages/app/src/apis.ts`. The following example shows an implementation that supports both public GitHub via `githubAuthApi` as well as a GitHub Enterprise installation hosted at `ghe.example.com` via `gheAuthApi`: + +```ts +createApiFactory({ + api: scmAuthApiRef, + deps: { + gheAuthApi: gheAuthApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory: ({ githubAuthApi, gheAuthApi }) => + ScmAuth.merge( + ScmAuth.forGithub(githubAuthApi), + ScmAuth.forGithub(gheAuthApi, { + host: 'ghe.example.com', + }), + ), +}); +``` diff --git a/docs/deployment/heroku.md b/docs/deployment/heroku.md index 42ab817b30..1545c34733 100644 --- a/docs/deployment/heroku.md +++ b/docs/deployment/heroku.md @@ -18,11 +18,10 @@ First, install the $ heroku login ``` -Heroku runs a container registry on `registry.heroku.com`. To push Backstage -Docker images, log in to the container registry also: +If you have not yet created a project through the Heroku interface, you can create it through the CLI. ```shell -$ heroku container:login +$ heroku create ``` You _might_ also need to set your Heroku app's stack to `container`: @@ -31,13 +30,43 @@ You _might_ also need to set your Heroku app's stack to `container`: $ heroku stack:set container -a ``` +Configuring your `app-config.yaml`: + +```yaml +app: + # Should be the same as backend.baseUrl when using the `app-backend` plugin + baseUrl: https://.herokuapp.com + +backend: + baseUrl: https://.herokuapp.com + listen: + port: + $env: PORT + # The $PORT environment variable is a feature of Heroku + # https://devcenter.heroku.com/articles/dynos#web-dynos +``` + +> Make sure your file is being copied into your container in the `Dockerfile`. + +Before building the Docker image, run the [backstage host build commands](https://backstage.io/docs/deployment/docker#host-build). They must be run whenever you are going to publish a new image. + +Heroku runs a container registry on `registry.heroku.com`. To push Backstage +Docker images, log in to the container registry also: + +```shell +$ heroku container:login +``` + ## Push and deploy a Docker image Now we can push a Backstage [Docker image](docker.md) to Heroku's container registry and release it to the `web` worker: ```bash -$ heroku container:push web -a +$ docker image build . -f packages/backend/Dockerfile --tag registry.heroku.com//web + +$ docker push registry.heroku.com//web + $ heroku container:release web -a ``` diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 285e399cb4..7c3717e22e 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -114,15 +114,13 @@ const routes = ( In `Root.tsx`, add the `SidebarSearchModal` component: ```bash -import { SidebarSearchModal, SearchContextProvider } from '@backstage/plugin-search'; +import { SidebarSearchModal } from '@backstage/plugin-search'; export const Root = ({ children }: PropsWithChildren<{}>) => ( - - - + ... ``` diff --git a/docs/features/software-catalog/api.md b/docs/features/software-catalog/api.md index f7c7b19112..3221b9e46a 100644 --- a/docs/features/software-catalog/api.md +++ b/docs/features/software-catalog/api.md @@ -1,7 +1,232 @@ --- id: software-catalog-api title: API -description: Documentation on Software Catalog API +description: The Software Catalog API --- -## TODO +The software catalog backend has a JSON based REST API, which can be leveraged +by external systems. This page describes its shape and features. + +## Overview + +The API surface consists of a few distinct groups of functionality. Each has a +dedicated section below. + +> **Note:** This page only describes some of the most commonly used parts of the +> API, and is a work in progress. + +All of the URL paths in this article are assumed to be on top of some base URL +pointing at your catalog installation. For example, if the path given in a +section below is `/entities`, and the catalog is located at +`http://localhost:7007/api/catalog` during local development, the full URL would +be `http://localhost:7007/api/catalog/entities`. The actual URL may vary from +one organization to the other, especially in production, but is commonly your +`backend.baseUrl` in your app config, plus `/api/catalog` at the end. + +Some or all of the endpoints may accept or require an `Authorization` header +with a `Bearer` token, which should then be the Backstage token returned by the +[`identity API`](https://backstage.io/docs/reference/core-plugin-api.identityapiref). + +## Entities + +These are the endpoints that deal with reading of entities directly. What it +exposes are final entities - i.e. the output of all processing and the stitching +process, not the raw originally ingested entity data. See [The Life of an +Entity](life-of-an-entity.md) for more details about this process and +distinction. + +### `GET /entities` + +Lists entities. Supports the following query parameters, described in sections +below: + +- [`filter`](#filtering), for selecting only a subset of all entities +- [`fields`](#field-selection), for selecting only parts of the full data + structure of each entity +- [`offset`, `limit`, and `after`](#pagination) for pagination + +The return type is JSON, as an array of [`Entity`](descriptor-format.md). + +#### Filtering + +You can pass in one or more filter sets that get matched against each entity. +Each filter set is a number of conditions that all have to match for the +condition to be true (conditions effectively have an AND between them). At least +one filter set has to be true for the entity to be part of the result set +(filter sets effectively have an OR between them). + +Example: + +```text +/entities?filter=kind=user,metadata.namespace=default&filter=kind=group,spec.type + + Return entities that match + + Filter set 1: + Condition 1: kind = user + AND + Condition 2: metadata.namespace = default + + OR + + Filter set 2: + Condition 1: kind = group + AND + Condition 2: spec.type exists +``` + +Each condition is either on the form ``, or on the form `=`. +The first form asserts on the existence of a certain key (with any value), and +the second asserts that the key exists and has a certain value. All checks are +always case _insensitive_. + +In all cases, the key is a simplified JSON path in a given piece of entity data. +Each part of the path is a key of an object, and the traversal also descends +through arrays. There are two special forms: + +- Array items that are simple value types (such as strings) match on a key-value + pair where the key is the item as a string, and the value is the string `true` +- Relations can be matched on a `relations.=` form + +Let's look at a simplified example to illustrate the concept: + +```json +{ + "a": { + "b": ["c", { "d": 1 }], + "e": 7 + } +} +``` + +This would match any one of the following conditions: + +- `a` +- `a.b` +- `a.b.c` +- `a.b.c=true` +- `a.b.d` +- `a.b.d=1` +- `a.e` +- `a.e=7` + +Some more real world usable examples: + +- Return all orphaned entities: + + `/entities?filter=metadata.annotations.backstage.io/orphan=true` + +- Return all users and groups: + + `/entities?filter=kind=user&filter=kind=group` + +- Return all service components: + + `/entities?filter=kind=component,spec.type=service` + +- Return all entities with the `java` tag: + + `/entities?filter=metadata.tags.java` + +- Return all users who are members of the `ops` group (note that the full + [reference](references.md) of the group is used): + + `/entities?filter=kind=user,relations.memberof=group:default/ops` + +#### Field selection + +By default the full entities are returned, but you can pass in a `fields` query +parameter which selects what parts of the entity data to retain. This makes the +response smaller and faster to transfer, and may allow the catalog to perform +more efficient queries. + +The query parameter value is a comma separated list of simplified JSON paths +like above. Each path corresponds to the key of either a value, or of a subtree +root that you want to keep in the output. The rest is pruned away. For example, +specifying `?fields=metadata.name,metadata.annotations,spec` retains only the +`name` and `annotations` fields of the `metadata` of each entity (it'll be an +object with at most two keys), keeps the entire `spec` unchanged, and cuts out +all other roots such as `relations`. + +Some more real world usable examples: + +- Return only enough data to form the full ref of each entity: + + `/entities?fields=kind,metadata.namespace,metadata.name` + +#### Pagination + +You may pass the `offset` and `limit` query parameters to do classical +pagination through the set of entities. There is also an `after` query parameter +to return the next page of results after the previous one when performing cursor +based pagination. + +Each paginated response that has a next page of data, will have a `Link`, +`rel="next"` header pointing to the query path to the next page. + +Example: Getting the first page: + +```text +GET /entities?limit=2 +HTTP/1.1 200 OK +link: ; rel="next" + +[{"metadata":{... +``` + +Getting the next page, since we detect the presence of the `Link` header: + +```text +GET /entities?limit=2&after=eyJsaW1pdCI6Miwib2Zmc2V0IjoyfQ%3D%3D +HTTP/1.1 200 OK +link: ; rel="next" + +[{"metadata":{... +``` + +### `GET /entities/by-uid/` + +Gets an entity by its `metadata.uid` field value. + +The return type is JSON, as a single [`Entity`](descriptor-format.md), or a 404 +error if there was no entity with that UID. + +### `DELETE /entities/by-uid/` + +Deletes an entity by its `metadata.uid` field value. + +> **Note:** This method of deletion is appropriate for orphaned entities, but +> not for removal of "live" entities that are actively being updated by a +> location. Please read below. + +The most common user flow is that you register a location (see below), and then +the catalog keeps itself up to date with that location and the subtree of things +that may spawn from it. This means that the catalog is a live-updating view of +an actual authoritative data source. If there's something keeping the entity +"alive" in the catalog, it will just reappear shortly after deletion with the +method described in this section. To properly remove entities, you typically +want to instead unregister the location that causes the entity to appear. + +However if you have an orphaned entity, for example after removing the reference +to its file from a `Location` entity, or if a processor has stopped producing +your entity, then this deletion method is appropriate. + +The return type is always an empty 204 response, whether an entity with this UID +existed or not. + +### `GET /entities/by-name///` + +Gets an entity by its `kind`, `metadata.namespace`, and `metadata.name` field +value. These are special in that they form the entity's unique +[reference](references.md) triplet. + +The return type is JSON, as a single [`Entity`](descriptor-format.md), or a 404 +error if there was no entity with that reference triplet. + +## Locations + +TODO + +## Other + +TODO diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index a38910df6c..70a8aeaa51 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1121,6 +1121,17 @@ This field is optional. | [`Component`](#kind-component) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) | | [`Resource`](#kind-resource) | Same as this entity, typically `default` | [`dependsOn`, and reverse `dependencyOf`](well-known-relations.md#dependson-and-dependencyof) | +### `spec.dependencyOf` [optional] + +An array of [entity references](references.md#string-references) to the +components and resources that the resource is a dependency of, e.g. `artist-lookup`. +This field is optional. + +| [`kind`](#apiversion-and-kind-required) | Default [`namespace`](#namespace-optional) | Generated [relation](well-known-relations.md) type | +| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------- | +| [`Component`](#kind-component) | Same as this entity, typically `default` | [`dependencyOf`, and reverse `dependsOn`](well-known-relations.md#dependson-and-dependencyof) | +| [`Resource`](#kind-resource) | Same as this entity, typically `default` | [`dependencyOf`, and reverse `dependsOn`](well-known-relations.md#dependson-and-dependencyof) | + ## Kind: System Describes the following entity kind: diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md index 9e8a3f0563..618923289e 100644 --- a/docs/features/software-catalog/life-of-an-entity.md +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -48,9 +48,9 @@ The details of these processes are described below. ## Ingestion -Each catalog deployment has a number of entity providers installed. They are +Each catalog deployment has a number of _entity providers_ installed. They are responsible for fetching data from external authoritative sources in any way -that they see fit, to translate those into entity objects, and to notify the +that they see fit, translating those into entity objects, and notifying the database when those entities are added or removed. These are the _unprocessed entities_ that will be subject to later processing (see below), and they form the very basis of existence for entities. If there were no entity providers, no @@ -108,7 +108,7 @@ the processing loop should next try to process it. When the entity first appears, this timestamp is set to "now" - asking for it to be picked up as soon as possible. -Each catalog deployment has a number of processors installed. They are +Each catalog deployment has a number of _processors_ installed. They are responsible for receiving unprocessed entities that the catalog decided are due for processing, and then running that data through a number of processing stages, mutating the entity and emitting auxiliary data about it. When all of @@ -185,7 +185,28 @@ happen during ingestion or processing. ## Errors -> TODO: Describe how errors are exposed through entities +Errors during the ingestion and processing of entities can happen in a variety +of ways, and they may happen at a far later point in time than when they were +registered. For example, a registered file may get deleted in the remote system, +or the user may accidentally change the file contents in such a way that they +cannot be parsed successfully, etc. + +There are two main ways that these errors are surfaced. + +First, the catalog backend will produce detailed logs that should contain +sufficient information for a reader to find the causes for errors. Since these +logs are typically not easily found by end users, this can mainly be a useful +tool for Backstage operators who want to debug problems either with statically +registered entities that are under their control, or to help end users find +problems. + +Second, for most classes of errors, the entity itself will contain a status +field that describes the problem. The contents of this field is shown at the top +of your entity page in Backstage, if you have placed the corresponding error +callout component (`EntityProcessingErrorsPanel`) there. + +We are still working to improve the surfacing and observability around +processing loop errors. ## Orphaning @@ -206,17 +227,22 @@ either, it becomes _orphaned_. The end result is as follows: - The catalog page in Backstage for the child entity detects the new annotation and informs users about the orphan status. -Orphaning can occur in several different scenarios. One common cause is that the -end user edited a corresponding catalog catalog-info YAML file removing the -entity's entry. In the case of a `Location` parent entity, orphaning can happen -if removing the target line pointing to the file containing the child entity. -Another common cause is large batch processors such as the ones that crawl -through remote systems looking for entities, no longer finding something that it -used to find before. Maybe the data was moved, or deleted, in the remote system. -So for example when a person leaves the company an LDAP org discovery processor -might leave an orphaned `User` entity behind. Note that this only applies to -processors - ingestion that happens using entity providers work differently, -described below. +Orphaning can occur in several different scenarios. + +- If a catalog-info YAML file is moved from one place to another in the version + control system without updating the registration in the catalog, it will + effectively become orphaned "by" that registered location +- If the user edits a corresponding parent catalog-info YAML file removing the + entity's entry - for example in the case of a `Location` parent entity, + orphaning can happen if editing or removing the `target`/`targets` lines + pointing to the file containing the child entity. +- Another common cause is large batch processors such as the ones that crawl + through remote systems looking for entities, no longer finding something that + it used to find before. Maybe the data was moved, or deleted, in the remote + system. So for example when a person leaves the company an LDAP org discovery + processor might leave an orphaned `User` entity behind. Note that this only + applies to processors - ingestion that happens using entity providers work + differently, described below. > Note that removing a file, or accidentally corrupting a file so that it cannot > be read successfully, does _not_ lead to orphaning. Hard errors, including the @@ -239,8 +265,78 @@ provided out of the box. ## Implicit Deletion -> TODO: Describe the process of entity providers eagerly deleting entities +Entity providers - not processors - are subject to _eager_ deletion of entities, +which may trigger the implicit deletion of more than just the entity you thought +you were deleting. This concept is explained here. + +Recall that all entity providers manage a private "bucket" of entities, as +described in the [External integrations](external-integrations.md) article. They +can perform some operations on those entities, including additions, updates, and +deletions. Entity additions/updates are subject to the regular processing loops, +which means that bucket entities may end up forming roots of an entire graph of +entities that are emitted by those processors as they recursively work they way +through the bucket contents and its descendants. + +When a provider issues a deletion of an entity in its bucket, that entity as +well as _the entire tree of entities processed out of it_, if any, are +considered for immediate deletion. Note "considered" - they are deleted if and +only if they would otherwise have become orphaned (no other parent entities +emitting them). Since the graph of entities is not strictly a tree, multiple +roots may actually end up indirectly referencing a node farther down in the +graph. If that's the case, that node won't go away until all such roots go away. + +URLs to yaml files that you register using either the Create button or add to +your app-config, are both handled by entity providers. That means that this +implicit deletion mechanism comes into play in some everyday circumstances. +Let's illustrate. + +Imagine that you have a monorepo, with a single `Location` entity in a +catalog-info file at the root, and that entity points to three other +catalog-info files in the repo with a `Component` entity in each one. + +```text +/ + feature_one/ + catalog-info.yaml <- kind: Component + feature_two/ + catalog-info.yaml <- kind: Component + feature_three/ + catalog-info.yaml <- kind: Component + catalog-info.yaml <- kind: Location +``` + +If you register the root `Location` entity, the actual effect is that _five_ +entities appear in the catalog. First, one that is named `generated-`-something, +which corresponds to the registered URL itself. That's the one that the provider +has put in its "bucket". Then, as processing loops chug along, the `Location` +entity you pointed to appears as a child of that, and then the three `Component` +entities appear in turn as children of the `Location`. + +As an end user of the Backstage interface, you may now want to delete one of the +three `Component` entities. You do that by visiting the three-dots menu in the +top right of an entity view. The popup dialog that appears will inform you that +actually this entity belongs to a certain root, and that you may want to remove +that root instead (which corresponds to unregistering the originally registered +URL). If you choose to do so, _all_ of the aforementioned five entities will +actually be deleted in the same operation. + +If you did not want to perform this aggressive pruning, you might have instead +chosen to remove one of the `target` rows of your `Location` catalog-info file, +and then deleted the catalog-info file that contained the `Component` you wanted +to get rid of. Now the catalog would be left with an orphaned component, and you +would instead be able to use the explicit deletion (see below) to delete that +single component. ## Explicit Deletion -> TODO: Describe direct deletion via the catalog API +The catalog and its REST API also permits direct deletion of individual +entities. This makes sense to do on orphaned entities; entities that aren't +being actively kept up to date by any parent entities. The popup interface under +the three-dots menu option of entity views does offer this option, and the +orphaned status can be seen in an info box at the top of the entity's overview +page. + +However, if you were to try to do an explicit depletion on an entity that's +being kept actively updated by a parent entity, it would just reappear again +shortly thereafter when the processing loops reconsider the parent entity that's +still in there. diff --git a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md index 4cc9a6a8e6..559cb036e0 100644 --- a/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md +++ b/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3.md @@ -34,6 +34,20 @@ The migration path is pretty simple, and we've removed some of the pain points from writing the `handlebars` templates too. Let's go through what's new and how to upgrade. +## Add the Processor to the `plugin-catalog-backend` + +An important change is to add the required processor to your `packages/backend/src/plugins/catalog.ts` + +```diff ++import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend'; + +... + + const builder = await CatalogBuilder.create(env); ++ builder.addProcessor(new ScaffolderEntitiesProcessor()); + const { processingEngine, router } = await builder.build(); +``` + ## `backstage.io/v1beta2` -> `scaffolder.backstage.io/v1beta3` The most important change is that you'll need to switch over the `apiVersion` in diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index a1dffe1580..415f8cd85e 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -287,6 +287,79 @@ The `RepoUrlPicker` is a custom field that we provide part of the `plugin-scaffolder`. You can provide your own custom fields by [writing your own Custom Field Extensions](./writing-custom-field-extensions.md) +##### Using the Users `oauth` token + +There's a little bit of extra magic that you get out of the box when using the +`RepoUrlPicker` as a field input. You can provide some additional options under +`ui:options` to allow the `RepoUrlPicker` to grab an `oauth` token for the user +for the required `repository`. + +This is great for when you are wanting to create a new repository, or wanting to +perform operations on top of an existing repository. + +A sample template that takes advantage of this is like so: + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: v1beta3-demo + title: Test Action template + description: scaffolder v1beta3 template demo +spec: + owner: backstage/techdocs-core + type: service + + parameters: + ... + + - title: Choose a location + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + # Here's the option you can pass to the RepoUrlPicker + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN + additionalScopes: + github: + - workflow:write + allowedHosts: + - github.com + ... + + steps: + ... + + - id: publish + name: Publish + action: publish:github + input: + allowedHosts: ['github.com'] + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} + # here's where the secret can be used + token: ${{ secrets.USER_OAUTH_TOKEN }} + + ... +``` + +You will see from above that there is an additional `requestUserCredentials` +object that is passed to the `RepoUrlPicker`. This object defines what the +returned `secret` should be stored as when accessing using +`${{ secrets.secretName }}`, in this case it is `USER_OAUTH_TOKEN`. And then you +will see that there is an additional `input` field into the `publish:github` +action called `token`, in which you can use the `secret` like so: +`token: ${{ secrets.USER_OAUTH_TOKEN }}`. + +There's also the ability to pass additional scopes when requesting the `oauth` +token from the user, which you can do on a per-provider basis, in case your +template can be published to multiple providers. + #### The Owner Picker When the scaffolder needs to add new components to the catalog, it needs to have diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index ec27a81653..383c3ae6d2 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -114,7 +114,7 @@ updated documentation next time you run Backstage! ## Writing and previewing your documentation -Using the [techdocs-cli](https://github.com/backstage/techdocs-cli) you can +Using the [techdocs-cli](https://github.com/backstage/backstage/tree/master/packages/techdocs-cli) you can preview your docs inside a local Backstage instance and get live reload on changes. This is useful when you want to preview your documentation while writing. diff --git a/docs/getting-started/keeping-backstage-updated.md b/docs/getting-started/keeping-backstage-updated.md index 107119aace..f16c62e28a 100644 --- a/docs/getting-started/keeping-backstage-updated.md +++ b/docs/getting-started/keeping-backstage-updated.md @@ -41,7 +41,10 @@ For this reason, any changes made to the template are documented along with upgrade instructions in the [changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md) of the `@backstage/create-app` package. We recommend peeking at this changelog -for any applicable updates when upgrading packages. +-for any applicable updates when upgrading packages. As an alternative, the +[Backstage Upgrade Helper](https://backstage.github.io/upgrade-helper/) provides +a consolidated view of all the changes between two versions of Backstage. You +can find the current version of your Backstage installation in `backstage.json`. ## More information on dependency mismatches diff --git a/docs/local-dev/cli-build-system.md b/docs/local-dev/cli-build-system.md index fdfede6e05..c4a08afc56 100644 --- a/docs/local-dev/cli-build-system.md +++ b/docs/local-dev/cli-build-system.md @@ -444,8 +444,9 @@ working directory to the package that the test is in. Where small customizations are needed, such as setting coverage thresholds or support for specific transforms, it is possible to override the Jest -configuration through the `"jest"` field in `package.json`. These overrides will -be loaded in from all `package.json` files in the directory ancestry, meaning +configuration through the `"jest"` field in `package.json`. For a full list of +options, see the [Jest documentation](https://jestjs.io/docs/en/configuration). +These overrides will be loaded in from all `package.json` files in the directory ancestry, meaning that you can place common configuration in the `package.json` at the root of a monorepo. If multiple overrides are found, they will be merged together with configuration further down in the directory tree taking precedence. @@ -464,6 +465,45 @@ The overrides in a single `package.json` may for example look like this: }, ``` +If you want to configure editor integration for tests we recommend executing the bundled configuration directly with Jest rather than running through the Yarn test script. For example, with the Jest extension for VS Code the configuration would look something like this: + +```jsonc +{ + "jest.jestCommandLine": "node_modules/.bin/jest --config node_modules/@backstage/cli/config/jest.js", + // In a large repo like the Backstage main repo you likely want to disable + // watch mode and the initial test run too, leaving just manual and perhaps + // on-save test runs in place. + "jest.autoRun": { + "watch": false, + "onSave": "test-src-file" + } +} +``` + +If you also want to enable source maps when debugging tests, you can do so by setting the `ENABLE_SOURCE_MAPS` environment variable. For example, a complete launch configuration for VS Code debugging may look like this: + +```json +{ + "type": "node", + "name": "vscode-jest-tests", + "request": "launch", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "program": "${workspaceFolder}/node_modules/.bin/jest", + "cwd": "${workspaceFolder}", + "env": { + "ENABLE_SOURCE_MAPS": "true" + }, + "args": [ + "--config", + "node_modules/@backstage/cli/config/jest.js", + "--runInBand", + "--watchAll=false" + ] +} +``` + ## Publishing Package publishing is an optional part of the Backstage build system and not diff --git a/docs/local-dev/cli-commands.md b/docs/local-dev/cli-commands.md index daf53ac0b2..eff6404e3b 100644 --- a/docs/local-dev/cli-commands.md +++ b/docs/local-dev/cli-commands.md @@ -440,22 +440,7 @@ a yarn workspaces monorepo by automatically creating one grouped configuration that includes all packages that have `backstage-cli test` in their package `test` script. -If needed, the configuration can be extended using a `"jest"` field in -`package.json`, both within the target package and the monorepo root, with -configuration in the target package taking precedence. Refer to the -[Jest configuration documentation](https://jestjs.io/docs/en/configuration) for -a full list of configuration options. - -In addition to the Jest configuration there's an optional `transformModules` -option, which is an array of module names to include in transformations. -Normally modules inside `node_modules` are not transformed, but there are cases -were published packages are not transpiled far enough to be usable by Jest, in -which case you need to enable transform of them. - -Another way to override the Jest configuration is to place a `jest.config.js` or -`jest.config.ts` file in the package root. As opposed to the `package.json` way -of overriding config, this completely removes the base config, and so you need -to set it up from scratch. +For more information about configuration overrides and editor support, see the [Jest Configuration section](./cli-build-system.md#jest-configuration) in the build system documentation. ```text Usage: backstage-cli test [options] diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 8a58677af6..d5fd02a46d 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -141,6 +141,63 @@ By convention, such packages should be named `@backstage/analytics-module-[name]`, and any configuration should be keyed under `app.analytics.[name]`. +### Handling User Identity + +If the analytics platform you are integrating with has a first-class concept of +user identity, you can (optionally) choose to support this by the following this +convention: + +- Allow your implementation to be instantiated with the `identityApi` as one of + its options in a `fromConfig` static method. +- Use the `userEntityRef` resolved by `identityApi`'s `getBackstageIdentity()` + method as the basis for the user ID you send to your analytics platform. + +For example: + +```typescript +import { + AnalyticsApi, + analyticsApiRef, + AnyApiFactory, + configApiRef, + createApiFactory, + identityApiRef, + IdentityApi, +} from '@backstage/core-plugin-api'; + +// Implementation that optionally initializes with a userId. +class AcmeAnalytics implements AnalyticsApi { + private constructor(accountId: number, identityApi?: IdentityApi) { + if (identityApi) { + identityApi.getBackstageIdentity().then(identity => { + AcmeAnalytics.init(accountId, { + userId: identity.userEntityRef, + }); + }); + } else { + AcmeAnalytics.init(accountId); + } + } + + static fromConfig(config, options) { + const accountId = config.getString('app.analytics.acme.id'); + return new AcmeAnalytics(accountId, options.identityApi); + } +} + +// Your implementation should be instantiated like this: +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: analyticsApiRef, + deps: { configApi: configApiRef, identityApi: identityApiRef }, + factory: ({ configApi, identityApi }) => + AcmeAnalytics.fromConfig(configApi, { + identityApi, + }), + }), +]; +``` + ## Capturing Events To instrument an event in a component, start by retrieving an analytics tracker diff --git a/docs/plugins/integrating-plugin-into-software-catalog.md b/docs/plugins/integrating-plugin-into-software-catalog.md index 71430e25a0..97efa56ac3 100644 --- a/docs/plugins/integrating-plugin-into-software-catalog.md +++ b/docs/plugins/integrating-plugin-into-software-catalog.md @@ -107,7 +107,7 @@ const systemPage = ( - + {/* Adding a new tab to the system view */} diff --git a/docs/publishing.md b/docs/publishing.md index ed83bddbbc..2951f7d30b 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -19,61 +19,60 @@ PR is merged. This is typically done every Thursday around noon CET. maintainers.** For this example we will be using the `@backstage/plugin-foo` package as an -example and assume that it is currently version `1.5.0` in the master branch. +example and assume that it is currently version `6.5.0` in the master branch. -In the event of a severe bug being introduced in version `1.5.0` of the -`@backstage/plugin-foo` released in the `2048-01-01` release, the following -process is used to release an emergency fix as `1.5.1`: +In the event of a severe bug being introduced in version `6.5.0` of the +`@backstage/plugin-foo` released in the `v1.18.0` Backstage release, the following +process is used to release an emergency fix as version `6.5.1` in the patch release `v1.18.1`: - [ ] Identify the release or releases that need to be patched. We should always - patch the most recent main-line release if needed, which in this example - would be `2048-01-01`. The fix may also need backporting to older major + patch the most recent major or minor main-line release if needed, which in this example + would be `v1.18.0`. The fix may also need backporting to older major versions, in which case we will want to patch the main-line release just before the one that bumped the package to each new major version. - [ ] Repeat the following steps for each release that needs to be patched: - [ ] Make sure a patch branch exists for the release that is being patched. If a patch already exists, reuse the existing branch. The branch **must - always** be named exactly `release--patch`. + always** be named exactly `patch/`. ```bash - git checkout release-2048-01-01 - git checkout -b release-2048-01-01-patch - git push --set-upstream origin release-2048-01-01-patch + git checkout v1.18.0 + git checkout -b patch/v1.18.0 + git push --set-upstream origin patch/v1.18.0 ``` - - [ ] With the `release-2048-01-01-patch` branch as a base, create a new + - [ ] With the `patch/v1.18.0` branch as a base, create a new branch for your fix. This branch can be named anything, but the following naming pattern may be suitable: ```bash - git checkout -b ${USER}/release-2048-01-01-emergency-fix + git checkout -b ${USER}/plugin-foo-v1.18.0-fix ``` - - [ ] Create a single commit that applies fixes and creates a new patch - changeset for the affected package. - - [ ] Run `yarn release` in the root of the repo in order to convert your - changeset into package version bumps and changelog entries. Commit these - changes as a second `"Generated release"` commit. - - [ ] Create PR towards the base branch (`release-2048-01-01-patch`) - containing the two commits. - - [ ] Review/Merge the PR into `release-2048-01-01-patch`. This will - automatically trigger a release. + - [ ] Create a single commit that applies fix, nothing else. + - [ ] Create a changeset for the affected package(s), then run `yarn release` in the root + of the repo in order to convert your changeset into package version bumps and changelog entries. + Commit these changes as a second `"Generated release"` commit. + - [ ] Create PR towards the base branch (`patch/v1.18.0`) containing the two commits. + - [ ] Review/Merge the PR into `patch/v1.18.0`. This will automatically trigger a release. -- [ ] Once fixes have been created for each release, the fix should be applied - to the master branch as well. Create a PR that contains the following: +- [ ] Look up the new version of our package in the patch PR as well as the new release + version, these can be found in the package `package.json` and the root `package.json`, and + will in this case be `6.5.1` and `v1.18.1`. You will need these versions later. +- [ ] Make sure you have the latest versions of the patch branch fetched, after merging the PR: `git fetch`. +- [ ] Once fixes have been created for each release, the fix should be applied to the + master branch as well. Create a PR that contains the following: - - [ ] The fix. - - [ ] A changeset with the message "Apply fix from the x.y.z patch release", - in this case `1.5.1`. You can find the version in your patch PR for the - most recent release. - - [ ] An entry in `.changeset/patched.json` that sets the current release to - that same version: + - [ ] The fix, which you can likely cherry-pick from your patch branch: `git cherry-pick origin/patch/v1.18.0^` + - [ ] An updated `CHANGELOG.md` of all patched patches from the tip of the patch branch, which you can get using `git checkout origin/patch/v1.18.0 -- plugins/foo/CHANGELOG.md`. + - [ ] A changeset with the message "Applied the fix from version `6.5.1` of this package, which is part of the `v1.18.1` release of Backstage.", + - [ ] An entry in `.changeset/patched.json` that sets the current release version to `6.5.1`: ```json { "currentReleaseVersion": { - "@backstage/plugin-foo": "1.5.1" + "@backstage/plugin-foo": "6.5.1" } } ``` diff --git a/microsite/pages/en/link.js b/microsite/pages/en/link.js index ddac35e545..727b15bfd8 100644 --- a/microsite/pages/en/link.js +++ b/microsite/pages/en/link.js @@ -5,6 +5,7 @@ const React = require('react'); const redirects = { 'bind-routes': '/docs/plugins/composability#binding-external-routes-in-the-app', + 'scm-auth': '/docs/auth/#custom-scmauthapi-implementation', }; const fallback = '/docs'; diff --git a/package.json b/package.json index 8701ac5f05..242c64c8bb 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "**/@roadiehq/**/@backstage/plugin-catalog": "*", "**/@roadiehq/**/@backstage/catalog-model": "*" }, - "version": "0.65.0", + "version": "0.67.0", "dependencies": { "@manypkg/get-packages": "^1.1.3", "@microsoft/api-documenter": "^7.15.0", @@ -68,7 +68,7 @@ "concurrently": "^7.0.0", "eslint-plugin-notice": "^0.9.10", "fs-extra": "9.1.0", - "husky": "^6.0.0", + "husky": "^7.0.4", "lerna": "^4.0.0", "lint-staged": "^12.2.0", "minimist": "^1.2.5", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index 4bca4d5182..74313008cd 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/app-defaults +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/core-app-api@0.5.2 + +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + - @backstage/core-app-api@0.5.2-next.0 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.1.5 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index ecd8d11bec..c7cb87faa4 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "0.1.5", + "version": "0.1.7", "private": false, "publishConfig": { "access": "public", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", - "@backstage/core-app-api": "^0.5.1", + "@backstage/core-components": "^0.8.8", + "@backstage/core-app-api": "^0.5.2", "@backstage/core-plugin-api": "^0.6.0", "@backstage/plugin-permission-react": "^0.3.0", "@backstage/theme": "^0.2.14", @@ -42,8 +42,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 56d38f8e5a..1139bf2cbb 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,210 @@ # example-app +## 0.2.64 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.13.2 + - @backstage/plugin-todo@0.2.0 + - @backstage/plugin-newrelic-dashboard@0.1.6 + - @backstage/core-components@0.8.8 + - @backstage/plugin-scaffolder@0.12.2 + - @backstage/plugin-search@0.6.2 + - @backstage/plugin-catalog-react@0.6.14 + - @backstage/plugin-catalog@0.7.12 + - @backstage/plugin-catalog-graph@0.2.10 + - @backstage/plugin-catalog-import@0.8.1 + - @backstage/plugin-home@0.4.14 + - @backstage/app-defaults@0.1.7 + - @backstage/integration-react@0.1.21 + - @backstage/plugin-airbrake@0.1.3 + - @backstage/plugin-apache-airflow@0.1.6 + - @backstage/plugin-api-docs@0.7.2 + - @backstage/plugin-azure-devops@0.1.14 + - @backstage/plugin-badges@0.2.22 + - @backstage/plugin-circleci@0.2.37 + - @backstage/plugin-cloudbuild@0.2.35 + - @backstage/plugin-code-coverage@0.1.25 + - @backstage/plugin-cost-insights@0.11.20 + - @backstage/plugin-explore@0.3.29 + - @backstage/plugin-gcp-projects@0.3.17 + - @backstage/plugin-github-actions@0.4.35 + - @backstage/plugin-gocd@0.1.4 + - @backstage/plugin-graphiql@0.2.30 + - @backstage/plugin-jenkins@0.5.20 + - @backstage/plugin-kafka@0.2.28 + - @backstage/plugin-kubernetes@0.5.7 + - @backstage/plugin-lighthouse@0.2.37 + - @backstage/plugin-newrelic@0.3.16 + - @backstage/plugin-pagerduty@0.3.25 + - @backstage/plugin-rollbar@0.3.26 + - @backstage/plugin-sentry@0.3.36 + - @backstage/plugin-shortcuts@0.1.22 + - @backstage/plugin-tech-insights@0.1.8 + - @backstage/plugin-tech-radar@0.5.5 + - @backstage/plugin-techdocs@0.13.3 + - @backstage/plugin-user-settings@0.3.19 + +## 0.2.64-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.13.2-next.0 + - @backstage/plugin-todo@0.2.0-next.0 + - @backstage/plugin-newrelic-dashboard@0.1.6-next.0 + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-scaffolder@0.12.2-next.0 + - @backstage/plugin-search@0.6.2-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + - @backstage/plugin-catalog-graph@0.2.10-next.0 + - @backstage/plugin-catalog-import@0.8.1-next.0 + - @backstage/plugin-home@0.4.14-next.0 + - @backstage/app-defaults@0.1.7-next.0 + - @backstage/integration-react@0.1.21-next.0 + - @backstage/plugin-airbrake@0.1.3-next.0 + - @backstage/plugin-apache-airflow@0.1.6-next.0 + - @backstage/plugin-api-docs@0.7.2-next.0 + - @backstage/plugin-azure-devops@0.1.14-next.0 + - @backstage/plugin-badges@0.2.22-next.0 + - @backstage/plugin-catalog@0.7.12-next.0 + - @backstage/plugin-circleci@0.2.37-next.0 + - @backstage/plugin-cloudbuild@0.2.35-next.0 + - @backstage/plugin-code-coverage@0.1.25-next.0 + - @backstage/plugin-cost-insights@0.11.20-next.0 + - @backstage/plugin-explore@0.3.29-next.0 + - @backstage/plugin-gcp-projects@0.3.17-next.0 + - @backstage/plugin-github-actions@0.4.35-next.0 + - @backstage/plugin-gocd@0.1.4-next.0 + - @backstage/plugin-graphiql@0.2.30-next.0 + - @backstage/plugin-jenkins@0.5.20-next.0 + - @backstage/plugin-kafka@0.2.28-next.0 + - @backstage/plugin-kubernetes@0.5.7-next.0 + - @backstage/plugin-lighthouse@0.2.37-next.0 + - @backstage/plugin-newrelic@0.3.16-next.0 + - @backstage/plugin-org@0.4.2-next.0 + - @backstage/plugin-pagerduty@0.3.25-next.0 + - @backstage/plugin-rollbar@0.3.26-next.0 + - @backstage/plugin-sentry@0.3.36-next.0 + - @backstage/plugin-shortcuts@0.1.22-next.0 + - @backstage/plugin-tech-insights@0.1.8-next.0 + - @backstage/plugin-tech-radar@0.5.5-next.0 + - @backstage/plugin-techdocs@0.13.3-next.0 + - @backstage/plugin-user-settings@0.3.19-next.0 + +## 0.2.63 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + - @backstage/plugin-code-coverage@0.1.24 + - @backstage/plugin-catalog-common@0.1.2 + - @backstage/cli@0.13.1 + - @backstage/plugin-scaffolder@0.12.1 + - @backstage/integration-react@0.1.20 + - @backstage/plugin-org@0.4.1 + - @backstage/plugin-pagerduty@0.3.24 + - @backstage/plugin-catalog-import@0.8.0 + - @backstage/core-app-api@0.5.2 + - @backstage/plugin-techdocs@0.13.2 + - @backstage/app-defaults@0.1.6 + - @backstage/plugin-airbrake@0.1.2 + - @backstage/plugin-apache-airflow@0.1.5 + - @backstage/plugin-api-docs@0.7.1 + - @backstage/plugin-azure-devops@0.1.13 + - @backstage/plugin-badges@0.2.21 + - @backstage/plugin-catalog@0.7.11 + - @backstage/plugin-catalog-graph@0.2.9 + - @backstage/plugin-circleci@0.2.36 + - @backstage/plugin-cloudbuild@0.2.34 + - @backstage/plugin-cost-insights@0.11.19 + - @backstage/plugin-explore@0.3.28 + - @backstage/plugin-gcp-projects@0.3.16 + - @backstage/plugin-github-actions@0.4.34 + - @backstage/plugin-gocd@0.1.3 + - @backstage/plugin-graphiql@0.2.29 + - @backstage/plugin-home@0.4.13 + - @backstage/plugin-jenkins@0.5.19 + - @backstage/plugin-kafka@0.2.27 + - @backstage/plugin-kubernetes@0.5.6 + - @backstage/plugin-lighthouse@0.2.36 + - @backstage/plugin-newrelic@0.3.15 + - @backstage/plugin-newrelic-dashboard@0.1.5 + - @backstage/plugin-rollbar@0.3.25 + - @backstage/plugin-search@0.6.1 + - @backstage/plugin-sentry@0.3.35 + - @backstage/plugin-shortcuts@0.1.21 + - @backstage/plugin-tech-insights@0.1.7 + - @backstage/plugin-tech-radar@0.5.4 + - @backstage/plugin-todo@0.1.21 + - @backstage/plugin-user-settings@0.3.18 + +## 0.2.63-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + - @backstage/plugin-catalog-react@0.6.13-next.1 + - @backstage/plugin-code-coverage@0.1.24-next.1 + - @backstage/plugin-catalog-common@0.1.2-next.0 + - @backstage/cli@0.13.1-next.1 + - @backstage/plugin-scaffolder@0.12.1-next.1 + - @backstage/core-app-api@0.5.2-next.0 + - @backstage/plugin-techdocs@0.13.2-next.1 + - @backstage/plugin-catalog@0.7.11-next.1 + - @backstage/app-defaults@0.1.6-next.1 + +## 0.2.63-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-org@0.4.1-next.0 + - @backstage/plugin-pagerduty@0.3.24-next.0 + - @backstage/plugin-catalog-import@0.8.0-next.0 + - @backstage/plugin-techdocs@0.13.2-next.0 + - @backstage/plugin-scaffolder@0.12.1-next.0 + - @backstage/cli@0.13.1-next.0 + - @backstage/app-defaults@0.1.6-next.0 + - @backstage/integration-react@0.1.20-next.0 + - @backstage/plugin-airbrake@0.1.2-next.0 + - @backstage/plugin-apache-airflow@0.1.5-next.0 + - @backstage/plugin-api-docs@0.7.1-next.0 + - @backstage/plugin-azure-devops@0.1.13-next.0 + - @backstage/plugin-badges@0.2.21-next.0 + - @backstage/plugin-catalog@0.7.11-next.0 + - @backstage/plugin-catalog-graph@0.2.9-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + - @backstage/plugin-circleci@0.2.36-next.0 + - @backstage/plugin-cloudbuild@0.2.34-next.0 + - @backstage/plugin-code-coverage@0.1.24-next.0 + - @backstage/plugin-cost-insights@0.11.19-next.0 + - @backstage/plugin-explore@0.3.28-next.0 + - @backstage/plugin-gcp-projects@0.3.16-next.0 + - @backstage/plugin-github-actions@0.4.34-next.0 + - @backstage/plugin-gocd@0.1.3-next.0 + - @backstage/plugin-graphiql@0.2.29-next.0 + - @backstage/plugin-home@0.4.13-next.0 + - @backstage/plugin-jenkins@0.5.19-next.0 + - @backstage/plugin-kafka@0.2.27-next.0 + - @backstage/plugin-kubernetes@0.5.6-next.0 + - @backstage/plugin-lighthouse@0.2.36-next.0 + - @backstage/plugin-newrelic@0.3.15-next.0 + - @backstage/plugin-newrelic-dashboard@0.1.5-next.0 + - @backstage/plugin-rollbar@0.3.25-next.0 + - @backstage/plugin-search@0.6.1-next.0 + - @backstage/plugin-sentry@0.3.35-next.0 + - @backstage/plugin-shortcuts@0.1.21-next.0 + - @backstage/plugin-tech-insights@0.1.7-next.0 + - @backstage/plugin-tech-radar@0.5.4-next.0 + - @backstage/plugin-todo@0.1.21-next.0 + - @backstage/plugin-user-settings@0.3.18-next.0 + ## 0.2.62 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 37ae4ad9bd..bdade7974a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,54 +1,56 @@ { "name": "example-app", - "version": "0.2.62", + "version": "0.2.64", "private": true, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^0.1.5", + "@backstage/app-defaults": "^0.1.7", "@backstage/catalog-model": "^0.9.10", - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/core-components": "^0.8.6", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/integration-react": "^0.1.19", - "@backstage/plugin-airbrake": "^0.1.1", - "@backstage/plugin-api-docs": "^0.7.0", - "@backstage/plugin-azure-devops": "^0.1.12", - "@backstage/plugin-apache-airflow": "^0.1.4", - "@backstage/plugin-badges": "^0.2.20", - "@backstage/plugin-catalog": "^0.7.10", - "@backstage/plugin-catalog-graph": "^0.2.8", - "@backstage/plugin-catalog-import": "^0.7.10", - "@backstage/plugin-catalog-react": "^0.6.12", - "@backstage/plugin-circleci": "^0.2.35", - "@backstage/plugin-cloudbuild": "^0.2.33", - "@backstage/plugin-code-coverage": "^0.1.23", - "@backstage/plugin-cost-insights": "^0.11.18", - "@backstage/plugin-explore": "^0.3.27", - "@backstage/plugin-gcp-projects": "^0.3.15", - "@backstage/plugin-github-actions": "^0.4.33", - "@backstage/plugin-gocd": "^0.1.2", - "@backstage/plugin-graphiql": "^0.2.28", - "@backstage/plugin-home": "^0.4.12", - "@backstage/plugin-jenkins": "^0.5.18", - "@backstage/plugin-kafka": "^0.2.26", - "@backstage/plugin-kubernetes": "^0.5.5", - "@backstage/plugin-lighthouse": "^0.2.35", - "@backstage/plugin-newrelic": "^0.3.14", - "@backstage/plugin-newrelic-dashboard": "^0.1.4", - "@backstage/plugin-org": "^0.4.0", - "@backstage/plugin-pagerduty": "0.3.23", - "@backstage/plugin-rollbar": "^0.3.24", - "@backstage/plugin-scaffolder": "^0.12.0", - "@backstage/plugin-search": "^0.6.0", - "@backstage/plugin-sentry": "^0.3.34", - "@backstage/plugin-shortcuts": "^0.1.20", - "@backstage/plugin-tech-radar": "^0.5.3", - "@backstage/plugin-techdocs": "^0.13.1", - "@backstage/plugin-todo": "^0.1.20", - "@backstage/plugin-user-settings": "^0.3.17", + "@backstage/integration-react": "^0.1.21", + "@backstage/plugin-airbrake": "^0.1.3", + "@backstage/plugin-api-docs": "^0.7.2", + "@backstage/plugin-azure-devops": "^0.1.14", + "@backstage/plugin-apache-airflow": "^0.1.6", + "@backstage/plugin-badges": "^0.2.22", + "@backstage/plugin-catalog": "^0.7.12", + "@backstage/plugin-catalog-common": "^0.1.2", + "@backstage/plugin-catalog-graph": "^0.2.10", + "@backstage/plugin-catalog-import": "^0.8.1", + "@backstage/plugin-catalog-react": "^0.6.14", + "@backstage/plugin-circleci": "^0.2.37", + "@backstage/plugin-cloudbuild": "^0.2.35", + "@backstage/plugin-code-coverage": "^0.1.25", + "@backstage/plugin-cost-insights": "^0.11.20", + "@backstage/plugin-explore": "^0.3.29", + "@backstage/plugin-gcp-projects": "^0.3.17", + "@backstage/plugin-github-actions": "^0.4.35", + "@backstage/plugin-gocd": "^0.1.4", + "@backstage/plugin-graphiql": "^0.2.30", + "@backstage/plugin-home": "^0.4.14", + "@backstage/plugin-jenkins": "^0.5.20", + "@backstage/plugin-kafka": "^0.2.28", + "@backstage/plugin-kubernetes": "^0.5.7", + "@backstage/plugin-lighthouse": "^0.2.37", + "@backstage/plugin-newrelic": "^0.3.16", + "@backstage/plugin-newrelic-dashboard": "^0.1.6", + "@backstage/plugin-org": "^0.4.2-next.0", + "@backstage/plugin-pagerduty": "0.3.25", + "@backstage/plugin-permission-react": "^0.3.0", + "@backstage/plugin-rollbar": "^0.3.26", + "@backstage/plugin-scaffolder": "^0.12.2", + "@backstage/plugin-search": "^0.6.2", + "@backstage/plugin-sentry": "^0.3.36", + "@backstage/plugin-shortcuts": "^0.1.22", + "@backstage/plugin-tech-radar": "^0.5.5", + "@backstage/plugin-techdocs": "^0.13.3", + "@backstage/plugin-todo": "^0.2.0", + "@backstage/plugin-user-settings": "^0.3.19", "@backstage/search-common": "^0.2.2", - "@backstage/plugin-tech-insights": "^0.1.6", + "@backstage/plugin-tech-insights": "^0.1.8", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -70,7 +72,7 @@ }, "devDependencies": { "@backstage/plugin-permission-react": "^0.3.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/test-utils": "^0.2.4", "@rjsf/core": "^3.2.1", "@testing-library/cypress": "^8.0.2", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 9be424a145..345915189a 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -86,6 +86,8 @@ import * as plugins from './plugins'; import { techDocsPage } from './components/techdocs/TechDocsPage'; import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; +import { PermissionedRoute } from '@backstage/plugin-permission-react'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; const app = createApp({ apis, @@ -140,7 +142,11 @@ const routes = ( > {entityPage} - } /> + } + /> ) => ( } to="/search"> - - - + }> diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 80e5b8f1a6..2e80c62c92 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/backend-common +## 0.10.7 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- 599f3dfa83: chore(deps-dev): bump `@types/concat-stream` from 1.6.1 to 2.0.0 +- c3868458d8: Removed unnecessary `get-port` dependency +- 04398e946e: Bump `selfsigned` to 2.0.0 + +## 0.10.7-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- 599f3dfa83: chore(deps-dev): bump `@types/concat-stream` from 1.6.1 to 2.0.0 +- c3868458d8: Removed unnecessary `get-port` dependency + +## 0.10.6 + +### Patch Changes + +- 50d039577a: Added a `Context` type for the backend, that can propagate an abort signal, a + deadline, and contextual values through the call stack. The main entrypoint is + the `Contexts` utility class that provides a root context creator and commonly + used decorators. + + These are marked as `@alpha` for now, and are therefore only accessible via + `@backstage/backend-common/alpha`. + +## 0.10.6-next.0 + +### Patch Changes + +- 50d039577a: Added a `Context` type for the backend, that can propagate an abort signal, a + deadline, and contextual values through the call stack. The main entrypoint is + the `Contexts` utility class that provides a root context creator and commonly + used decorators. + + These are marked as `@alpha` for now, and are therefore only accessible via + `@backstage/backend-common/alpha`. + ## 0.10.5 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index d63416571d..9b963f3fc9 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -6,6 +6,7 @@ /// /// +import { AbortController as AbortController_2 } from 'node-abort-controller'; import { AbortSignal as AbortSignal_2 } from 'node-abort-controller'; import { AwsS3Integration } from '@backstage/integration'; import { AzureIntegration } from '@backstage/integration'; @@ -13,6 +14,7 @@ import { BitbucketIntegration } from '@backstage/integration'; import { Config } from '@backstage/config'; import cors from 'cors'; import Docker from 'dockerode'; +import { Duration } from 'luxon'; import { ErrorRequestHandler } from 'express'; import express from 'express'; import { GithubCredentialsProvider } from '@backstage/integration'; @@ -145,6 +147,29 @@ export interface ContainerRunner { runContainer(opts: RunContainerOptions): Promise; } +// @alpha +export interface Context { + readonly abortSignal: AbortSignal_2; + readonly deadline: Date | undefined; + value(key: string): T | undefined; +} + +// @alpha +export class Contexts { + static root(): Context; + static withAbort( + parentCtx: Context, + source: AbortController_2 | AbortSignal_2, + ): Context; + static withTimeoutDuration(parentCtx: Context, timeout: Duration): Context; + static withTimeoutMillis(parentCtx: Context, timeout: number): Context; + static withValue( + parentCtx: Context, + key: string, + value: unknown | ((previous: unknown | undefined) => unknown), + ): Context; +} + // @public @deprecated export const createDatabase: typeof createDatabaseClient; @@ -152,7 +177,7 @@ export const createDatabase: typeof createDatabaseClient; export function createDatabaseClient( dbConfig: Config, overrides?: Partial, -): Knex; +): Knex[]>; // @public export function createRootLogger( diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 4040b778bf..13a34a1d81 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,14 +1,15 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.10.5", + "version": "0.10.7", "main": "src/index.ts", "types": "src/index.ts", "private": false, "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "homepage": "https://backstage.io", "repository": { @@ -21,7 +22,7 @@ ], "license": "Apache-2.0", "scripts": { - "build": "backstage-cli build --outputs cjs,types", + "build": "backstage-cli build --experimental-type-build --outputs cjs,types", "lint": "backstage-cli lint", "test": "backstage-cli test", "prepack": "backstage-cli prepack", @@ -41,6 +42,7 @@ "@types/cors": "^2.8.6", "@types/dockerode": "^3.3.0", "@types/express": "^4.17.6", + "@types/luxon": "^2.0.4", "archiver": "^5.0.2", "aws-sdk": "^2.840.0", "compression": "^1.7.4", @@ -56,16 +58,17 @@ "jose": "^1.27.1", "keyv": "^4.0.3", "keyv-memcache": "^1.2.5", - "knex": "^0.95.1", + "knex": "^1.0.2", "lodash": "^4.17.21", "logform": "^2.3.2", + "luxon": "^2.0.2", "minimatch": "^3.0.4", "minimist": "^1.2.5", "morgan": "^1.10.0", "node-abort-controller": "^3.0.1", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "raw-body": "^2.4.1", - "selfsigned": "^1.10.7", + "selfsigned": "^2.0.0", "stoppable": "^1.1.0", "tar": "^6.1.2", "unzipper": "^0.10.11", @@ -81,11 +84,11 @@ } }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/test-utils": "^0.2.4", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", - "@types/concat-stream": "^1.6.0", + "@types/concat-stream": "^2.0.0", "@types/fs-extra": "^9.0.3", "@types/http-errors": "^1.6.3", "@types/minimist": "^1.2.0", @@ -98,7 +101,6 @@ "@types/unzipper": "^0.10.3", "@types/webpack-env": "^1.15.2", "aws-sdk-mock": "^5.2.1", - "get-port": "^5.1.1", "http-errors": "^2.0.0", "jest": "^26.0.1", "mock-fs": "^5.1.0", @@ -109,7 +111,8 @@ }, "files": [ "dist", - "config.d.ts" + "config.d.ts", + "alpha" ], "configSchema": "config.d.ts" } diff --git a/packages/backend-common/src/context/AbortContext.test.ts b/packages/backend-common/src/context/AbortContext.test.ts new file mode 100644 index 0000000000..f47e1d4beb --- /dev/null +++ b/packages/backend-common/src/context/AbortContext.test.ts @@ -0,0 +1,345 @@ +/* + * 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 { AbortController } from 'node-abort-controller'; +import { AbortContext } from './AbortContext'; +import { RootContext } from './RootContext'; + +describe('AbortContext', () => { + afterEach(() => { + jest.useRealTimers(); + }); + + describe('forTimeoutMillis', () => { + it('can abort on a timeout', async () => { + jest.useFakeTimers(); + const timeout = 200; + const deadline = Date.now() + timeout; + + const root = new RootContext(); + + const child = AbortContext.forTimeoutMillis(root, timeout); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(child.abortSignal.aborted).toBe(false); + expect(Math.abs(+child.deadline! - deadline)).toBeLessThan(50); + expect(childListener).toBeCalledTimes(0); + + jest.advanceTimersByTime(timeout + 1); + + expect(child.abortSignal.aborted).toBe(true); + expect(childListener).toBeCalledTimes(1); + }); + + it('results in minimum deadline when parent triggers sooner', async () => { + jest.useFakeTimers(); + const parentTimeout = 200; + const childTimeout = 300; + const parentDeadline = Date.now() + parentTimeout; + const childDeadline = parentDeadline; // clamped + + const root = new RootContext(); + + const parent = AbortContext.forTimeoutMillis(root, parentTimeout); + const parentListener = jest.fn(); + parent.abortSignal.addEventListener('abort', parentListener); + + const child = AbortContext.forTimeoutMillis(parent, childTimeout); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(parent.abortSignal.aborted).toBe(false); + expect(child.abortSignal.aborted).toBe(false); + expect(Math.abs(+parent.deadline! - parentDeadline)).toBeLessThan(50); + expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50); + expect(parentListener).toBeCalledTimes(0); + expect(childListener).toBeCalledTimes(0); + + jest.advanceTimersByTime(parentTimeout + 1); + + expect(parent.abortSignal.aborted).toBe(true); + expect(child.abortSignal.aborted).toBe(true); + expect(parentListener).toBeCalledTimes(1); + expect(childListener).toBeCalledTimes(1); + }); + + it('results in minimum deadline when child triggers sooner', async () => { + jest.useFakeTimers(); + const parentTimeout = 300; + const childTimeout = 200; + const parentDeadline = Date.now() + parentTimeout; + const childDeadline = Date.now() + childTimeout; + + const root = new RootContext(); + + const parent = AbortContext.forTimeoutMillis(root, parentTimeout); + const parentListener = jest.fn(); + parent.abortSignal.addEventListener('abort', parentListener); + + const child = AbortContext.forTimeoutMillis(parent, childTimeout); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(parent.abortSignal.aborted).toBe(false); + expect(child.abortSignal.aborted).toBe(false); + expect(Math.abs(+parent.deadline! - parentDeadline)).toBeLessThan(50); + expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50); + expect(parentListener).toBeCalledTimes(0); + expect(childListener).toBeCalledTimes(0); + + jest.advanceTimersByTime(childTimeout + 1); + + expect(parent.abortSignal.aborted).toBe(false); + expect(child.abortSignal.aborted).toBe(true); + expect(parentListener).toBeCalledTimes(0); + expect(childListener).toBeCalledTimes(1); + + jest.advanceTimersByTime(parentTimeout - childTimeout + 1); + + expect(parent.abortSignal.aborted).toBe(true); + expect(child.abortSignal.aborted).toBe(true); + expect(parentListener).toBeCalledTimes(1); + expect(childListener).toBeCalledTimes(1); + }); + + it('child carries over parent signal state if parent was already aborted and had no deadline', async () => { + jest.useFakeTimers(); + const childTimeout = 200; + const childDeadline = Date.now() + childTimeout; + + const root = new RootContext(); + + const parentController = new AbortController(); + const parent = AbortContext.forSignal(root, parentController.signal); + + parentController.abort(); + + const child = AbortContext.forTimeoutMillis(parent, childTimeout); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(child.abortSignal.aborted).toBe(true); + expect(childListener).toBeCalledTimes(0); + expect(Math.abs(+child.deadline! - childDeadline)).toBeLessThan(50); + + jest.advanceTimersByTime(childTimeout + 1); + + expect(child.abortSignal.aborted).toBe(true); + expect(childListener).toBeCalledTimes(0); // still + }); + + it('child carries over parent signal state if parent was already aborted and had a deadline', async () => { + jest.useFakeTimers(); + const first = new RootContext(); + + const secondController = new AbortController(); + const second = AbortContext.forSignal(first, secondController.signal); + secondController.abort(); + + const third = AbortContext.forTimeoutMillis(second, 200); + const fourth = AbortContext.forTimeoutMillis(third, 300); + + expect(third.abortSignal.aborted).toBe(true); + expect(fourth.abortSignal.aborted).toBe(true); + expect(Math.abs(+fourth.deadline! - Date.now() - 200)).toBeLessThan(50); + }); + }); + + describe('forController', () => { + it('signals child when parent is aborted', () => { + const root = new RootContext(); + + const parentController = new AbortController(); + const parent = AbortContext.forController(root, parentController); + const parentListener = jest.fn(); + parent.abortSignal.addEventListener('abort', parentListener); + + const childController = new AbortController(); + const child = AbortContext.forController(parent, childController); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(parent.abortSignal.aborted).toBe(false); + expect(child.abortSignal.aborted).toBe(false); + expect(parentListener).toBeCalledTimes(0); + expect(childListener).toBeCalledTimes(0); + + parentController.abort(); + + expect(parent.abortSignal.aborted).toBe(true); + expect(child.abortSignal.aborted).toBe(true); + expect(parentListener).toBeCalledTimes(1); + expect(childListener).toBeCalledTimes(1); + }); + + it('does not signal parent when child is aborted', async () => { + const root = new RootContext(); + + const parentController = new AbortController(); + const parent = AbortContext.forController(root, parentController); + const parentListener = jest.fn(); + parent.abortSignal.addEventListener('abort', parentListener); + + const childController = new AbortController(); + const child = AbortContext.forController(parent, childController); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(parent.abortSignal.aborted).toBe(false); + expect(child.abortSignal.aborted).toBe(false); + expect(parentListener).toBeCalledTimes(0); + expect(childListener).toBeCalledTimes(0); + + childController.abort(); + + expect(parent.abortSignal.aborted).toBe(false); + expect(child.abortSignal.aborted).toBe(true); + expect(parentListener).toBeCalledTimes(0); + expect(childListener).toBeCalledTimes(1); + }); + + it('child carries over parent signal state if parent was already aborted', async () => { + const root = new RootContext(); + + const parentController = new AbortController(); + const parent = AbortContext.forController(root, parentController); + + parentController.abort(); + + const childController = new AbortController(); + const child = AbortContext.forController(parent, childController); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(parent.abortSignal.aborted).toBe(true); + expect(child.abortSignal.aborted).toBe(true); + expect(childListener).toBeCalledTimes(0); + + childController.abort(); + + expect(parent.abortSignal.aborted).toBe(true); + expect(child.abortSignal.aborted).toBe(true); + expect(childListener).toBeCalledTimes(0); + }); + + it('child carries over given signal state if it was already aborted', async () => { + const root = new RootContext(); + + const childController = new AbortController(); + childController.abort(); + + const child = AbortContext.forController(root, childController); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(child.abortSignal.aborted).toBe(true); + expect(childListener).toBeCalledTimes(0); + }); + }); + + describe('forSignal', () => { + it('signals child when parent is aborted', async () => { + const root = new RootContext(); + + const parentController = new AbortController(); + const parent = AbortContext.forSignal(root, parentController.signal); + const parentListener = jest.fn(); + parent.abortSignal.addEventListener('abort', parentListener); + + const childController = new AbortController(); + const child = AbortContext.forSignal(parent, childController.signal); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(parent.abortSignal.aborted).toBe(false); + expect(child.abortSignal.aborted).toBe(false); + expect(parentListener).toBeCalledTimes(0); + expect(childListener).toBeCalledTimes(0); + + parentController.abort(); + + expect(parent.abortSignal.aborted).toBe(true); + expect(child.abortSignal.aborted).toBe(true); + expect(parentListener).toBeCalledTimes(1); + expect(childListener).toBeCalledTimes(1); + }); + + it('does not signal parent when child is aborted', async () => { + const root = new RootContext(); + + const parentController = new AbortController(); + const parent = AbortContext.forSignal(root, parentController.signal); + const parentListener = jest.fn(); + parent.abortSignal.addEventListener('abort', parentListener); + + const childController = new AbortController(); + const child = AbortContext.forSignal(parent, childController.signal); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(parent.abortSignal.aborted).toBe(false); + expect(child.abortSignal.aborted).toBe(false); + expect(parentListener).toBeCalledTimes(0); + expect(childListener).toBeCalledTimes(0); + + childController.abort(); + + expect(parent.abortSignal.aborted).toBe(false); + expect(child.abortSignal.aborted).toBe(true); + expect(parentListener).toBeCalledTimes(0); + expect(childListener).toBeCalledTimes(1); + }); + + it('child carries over parent signal state if parent was already aborted', async () => { + const root = new RootContext(); + + const parentController = new AbortController(); + const parent = AbortContext.forSignal(root, parentController.signal); + + parentController.abort(); + + const childController = new AbortController(); + const child = AbortContext.forSignal(parent, childController.signal); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(parent.abortSignal.aborted).toBe(true); + expect(child.abortSignal.aborted).toBe(true); + expect(childListener).toBeCalledTimes(0); + + childController.abort(); + + expect(parent.abortSignal.aborted).toBe(true); + expect(child.abortSignal.aborted).toBe(true); + expect(childListener).toBeCalledTimes(0); + }); + + it('child carries over given signal state if it was already aborted', async () => { + const root = new RootContext(); + + const childController = new AbortController(); + childController.abort(); + + const child = AbortContext.forSignal(root, childController.signal); + const childListener = jest.fn(); + child.abortSignal.addEventListener('abort', childListener); + + expect(child.abortSignal.aborted).toBe(true); + expect(childListener).toBeCalledTimes(0); + }); + }); +}); diff --git a/packages/backend-common/src/context/AbortContext.ts b/packages/backend-common/src/context/AbortContext.ts new file mode 100644 index 0000000000..8a119358a6 --- /dev/null +++ b/packages/backend-common/src/context/AbortContext.ts @@ -0,0 +1,133 @@ +/* + * 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 { AbortController, AbortSignal } from 'node-abort-controller'; +import { Context } from './types'; + +/** + * A context that implements various abort related functionality. + */ +export class AbortContext implements Context { + /** + * Abort either when the parent aborts, or after the given timeout has + * expired. + * + * @param ctx - The parent context + * @param timeout - A timeout value, in milliseconds + * @returns A new context + */ + static forTimeoutMillis(ctx: Context, timeout: number): Context { + const desiredDeadline = new Date(Date.now() + timeout); + const actualDeadline = + ctx.deadline && ctx.deadline < desiredDeadline + ? ctx.deadline + : desiredDeadline; + + if (ctx.abortSignal.aborted) { + if (ctx.deadline && desiredDeadline === actualDeadline) { + return ctx; + } + return new AbortContext(ctx, ctx.abortSignal, actualDeadline); + } + + const controller = new AbortController(); + const timeoutHandle = setTimeout(abort, timeout); + ctx.abortSignal.addEventListener('abort', abort); + + function abort() { + ctx.abortSignal.removeEventListener('abort', abort); + clearTimeout(timeoutHandle!); + controller.abort(); + } + + return new AbortContext(ctx, controller.signal, actualDeadline); + } + + /** + * Abort either when the parent aborts, or when the given controller is + * triggered. + * + * @remarks + * + * If you have access to the controller, this function is more efficient than + * {@link AbortContext#forSignal}. + * + * @param ctx - The parent context + * @param controller - An abort controller + * @returns A new context + */ + static forController(ctx: Context, controller: AbortController): Context { + // Already aborted context / signal are fine to reuse as-is + if (ctx.abortSignal.aborted) { + return ctx; + } else if (controller.signal.aborted) { + return new AbortContext(ctx, controller.signal, ctx.deadline); + } + + function abort() { + ctx.abortSignal.removeEventListener('abort', abort); + controller.abort(); + } + + ctx.abortSignal.addEventListener('abort', abort); + + return new AbortContext(ctx, controller.signal, ctx.deadline); + } + + /** + * Abort either when the parent aborts, or when the given signal is triggered. + * + * @remarks + * + * If you have access to the controller and not just the signal, + * {@link AbortContext#forController} is slightly more efficient to use. + * + * @param ctx - The parent context + * @param signal - An abort signal + * @returns A new context + */ + static forSignal(ctx: Context, signal: AbortSignal): Context { + // Already aborted context / signal are fine to reuse as-is + if (ctx.abortSignal.aborted) { + return ctx; + } else if (signal.aborted) { + return new AbortContext(ctx, signal, ctx.deadline); + } + + const controller = new AbortController(); + + function abort() { + ctx.abortSignal.removeEventListener('abort', abort); + signal.removeEventListener('abort', abort); + controller.abort(); + } + + ctx.abortSignal.addEventListener('abort', abort); + signal.addEventListener('abort', abort); + + return new AbortContext(ctx, controller.signal, ctx.deadline); + } + + private constructor( + private readonly parent: Context, + readonly abortSignal: AbortSignal, + readonly deadline: Date | undefined, + ) {} + + value(key: string): T | undefined { + return this.parent.value(key); + } +} diff --git a/packages/backend-common/src/context/Contexts.test.ts b/packages/backend-common/src/context/Contexts.test.ts new file mode 100644 index 0000000000..cbd61d20a9 --- /dev/null +++ b/packages/backend-common/src/context/Contexts.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { Duration } from 'luxon'; +import { AbortController } from 'node-abort-controller'; +import { Contexts } from './Contexts'; + +describe('Contexts', () => { + afterEach(() => { + jest.useRealTimers(); + }); + + describe('root', () => { + it('can create a root', () => { + const ctx = Contexts.root(); + expect(ctx.abortSignal).toBeDefined(); + expect(ctx.deadline).toBeUndefined(); + }); + }); + + describe('setAbort', () => { + it('works for controllers', () => { + const controller = new AbortController(); + const parent = Contexts.root(); + const child = Contexts.withAbort(parent, controller); + expect(child.abortSignal.aborted).toBe(false); + controller.abort(); + expect(child.abortSignal.aborted).toBe(true); + }); + + it('works for signals', () => { + const controller = new AbortController(); + const parent = Contexts.root(); + const child = Contexts.withAbort(parent, controller.signal); + expect(child.abortSignal.aborted).toBe(false); + controller.abort(); + expect(child.abortSignal.aborted).toBe(true); + }); + }); + + describe('setTimeoutDuration', () => { + it('works', () => { + jest.useFakeTimers(); + const parent = Contexts.root(); + const child = Contexts.withTimeoutDuration( + parent, + Duration.fromMillis(200), + ); + expect(child.abortSignal.aborted).toBe(false); + jest.advanceTimersByTime(100); + expect(child.abortSignal.aborted).toBe(false); + jest.advanceTimersByTime(101); + expect(child.abortSignal.aborted).toBe(true); + }); + }); + + describe('setTimeoutMillis', () => { + it('works', () => { + jest.useFakeTimers(); + const parent = Contexts.root(); + const child = Contexts.withTimeoutMillis(parent, 200); + expect(child.abortSignal.aborted).toBe(false); + jest.advanceTimersByTime(100); + expect(child.abortSignal.aborted).toBe(false); + jest.advanceTimersByTime(101); + expect(child.abortSignal.aborted).toBe(true); + }); + }); + + describe('setValue', () => { + it('works', () => { + const parent = Contexts.root(); + const child = Contexts.withValue(parent, 'k', 'v'); + expect(child.value('k')).toBe('v'); + }); + }); +}); diff --git a/packages/backend-common/src/context/Contexts.ts b/packages/backend-common/src/context/Contexts.ts new file mode 100644 index 0000000000..e22a76f673 --- /dev/null +++ b/packages/backend-common/src/context/Contexts.ts @@ -0,0 +1,114 @@ +/* + * 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 { Duration } from 'luxon'; +import { AbortController, AbortSignal } from 'node-abort-controller'; +import { AbortContext } from './AbortContext'; +import { RootContext } from './RootContext'; +import { Context } from './types'; +import { ValueContext } from './ValueContext'; + +/** + * Common context decorators. + * + * @alpha + */ +export class Contexts { + /** + * Creates a root context. + * + * @remarks + * + * This should normally only be called near the root of an application. The + * created context is meant to be passed down into deeper levels, which may or + * may not make derived contexts out of it. + */ + static root(): Context { + return new RootContext(); + } + + /** + * Creates a derived context, which signals to abort operations either when + * any parent context signals, or when the given source is aborted. + * + * @remarks + * + * If the parent context was already aborted, then it is returned as-is. + * + * If the given source was already aborted, then a new already-aborted context + * is returned. + * + * @param parentCtx - A parent context that shall be used as a base + * @param source - An abort controller or signal that you intend to perhaps + * trigger at some later point in time. + * @returns A new {@link Context} + */ + static withAbort( + parentCtx: Context, + source: AbortController | AbortSignal, + ): Context { + return 'aborted' in source + ? AbortContext.forSignal(parentCtx, source) + : AbortContext.forController(parentCtx, source); + } + + /** + * Creates a derived context, which signals to abort operations either when + * any parent context signals, or when the given amount of time has passed. + * This may affect the deadline. + * + * @param parentCtx - A parent context that shall be used as a base + * @param timeout - The duration of time, after which the derived context will + * signal to abort. + * @returns A new {@link Context} + */ + static withTimeoutDuration(parentCtx: Context, timeout: Duration): Context { + return AbortContext.forTimeoutMillis(parentCtx, timeout.as('milliseconds')); + } + + /** + * Creates a derived context, which signals to abort operations either when + * any parent context signals, or when the given amount of time has passed. + * This may affect the deadline. + * + * @param parentCtx - A parent context that shall be used as a base + * @param timeout - The number of milliseconds, after which the derived + * context will signal to abort. + * @returns A new {@link Context} + */ + static withTimeoutMillis(parentCtx: Context, timeout: number): Context { + return AbortContext.forTimeoutMillis(parentCtx, timeout); + } + + /** + * Creates a derived context, which has a specific key-value pair set as well + * as all key-value pairs that were set in the original context. + * + * @param parentCtx - A parent context that shall be used as a base + * @param key - The key of the value to set + * @param value - The value, or a function that accepts the previous value (or + * undefined if not set yet) and computes the new value + * @returns A new {@link Context} + */ + static withValue( + parentCtx: Context, + key: string, + value: unknown | ((previous: unknown | undefined) => unknown), + ): Context { + const v = typeof value === 'function' ? value(parentCtx.value(key)) : value; + return ValueContext.forConstantValue(parentCtx, key, v); + } +} diff --git a/packages/backend-common/src/context/RootContext.test.ts b/packages/backend-common/src/context/RootContext.test.ts new file mode 100644 index 0000000000..395786e3dc --- /dev/null +++ b/packages/backend-common/src/context/RootContext.test.ts @@ -0,0 +1,27 @@ +/* + * 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 { RootContext } from './RootContext'; + +describe('RootContext', () => { + it('returns empty values', async () => { + const ctx = new RootContext(); + expect(ctx.abortSignal).toBeDefined(); + expect(ctx.abortSignal.aborted).toBe(false); + expect(ctx.deadline).toBeUndefined(); + expect(ctx.value('a')).toBeUndefined(); + }); +}); diff --git a/packages/backend-common/src/context/RootContext.ts b/packages/backend-common/src/context/RootContext.ts new file mode 100644 index 0000000000..61962380a4 --- /dev/null +++ b/packages/backend-common/src/context/RootContext.ts @@ -0,0 +1,45 @@ +/* + * 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 { AbortSignal } from 'node-abort-controller'; +import { Context } from './types'; + +/** + * Since the root context can never abort, and since nobody is every meant to + * dispatch events through it, we can use a static dummy instance for + * efficiency. + */ +const dummyAbortSignal: AbortSignal = Object.freeze({ + aborted: false, + addEventListener() {}, + removeEventListener() {}, + dispatchEvent() { + return true; + }, + onabort: null, +}); + +/** + * An empty root context. + */ +export class RootContext implements Context { + readonly abortSignal = dummyAbortSignal; + readonly deadline = undefined; + + value(_key: string): T | undefined { + return undefined; + } +} diff --git a/packages/backend-common/src/context/ValueContext.test.ts b/packages/backend-common/src/context/ValueContext.test.ts new file mode 100644 index 0000000000..6a216d4548 --- /dev/null +++ b/packages/backend-common/src/context/ValueContext.test.ts @@ -0,0 +1,44 @@ +/* + * 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 { RootContext } from './RootContext'; +import { ValueContext } from './ValueContext'; + +describe('ValueContext', () => { + it('returns its own values, or delegates to the parent', async () => { + const root = new RootContext(); + const a = ValueContext.forConstantValue(root, 'a', 1); + const b = ValueContext.forConstantValue(a, 'x', 2); + const c = ValueContext.forConstantValue(b, 'a', 3); + const d = ValueContext.forConstantValue(c, 'b', 4); + + expect(a.value('a')).toBe(1); + expect(a.value('b')).toBeUndefined(); + expect(a.value('x')).toBeUndefined(); + + expect(b.value('a')).toBe(1); + expect(b.value('b')).toBeUndefined(); + expect(b.value('x')).toBe(2); + + expect(c.value('a')).toBe(3); + expect(c.value('b')).toBeUndefined(); + expect(c.value('x')).toBe(2); + + expect(d.value('a')).toBe(3); + expect(d.value('b')).toBe(4); + expect(d.value('x')).toBe(2); + }); +}); diff --git a/packages/backend-common/src/context/ValueContext.ts b/packages/backend-common/src/context/ValueContext.ts new file mode 100644 index 0000000000..441aaec984 --- /dev/null +++ b/packages/backend-common/src/context/ValueContext.ts @@ -0,0 +1,46 @@ +/* + * 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 { AbortSignal } from 'node-abort-controller'; +import { Context } from './types'; + +/** + * A context that just holds a single value, and delegates the rest to its + * parent. + */ +export class ValueContext implements Context { + static forConstantValue(ctx: Context, key: string, value: unknown): Context { + return new ValueContext(ctx, key, value); + } + + constructor( + private readonly _parent: Context, + private readonly _key: string, + private readonly _value: unknown, + ) {} + + get abortSignal(): AbortSignal { + return this._parent.abortSignal; + } + + get deadline(): Date | undefined { + return this._parent.deadline; + } + + value(key: string): T | undefined { + return key === this._key ? (this._value as T) : this._parent.value(key); + } +} diff --git a/plugins/todo/src/plugin.test.ts b/packages/backend-common/src/context/index.ts similarity index 80% rename from plugins/todo/src/plugin.test.ts rename to packages/backend-common/src/context/index.ts index a99373abc6..37a6e29c8c 100644 --- a/plugins/todo/src/plugin.test.ts +++ b/packages/backend-common/src/context/index.ts @@ -13,10 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { todoPlugin } from './plugin'; -describe('todo', () => { - it('should export plugin', () => { - expect(todoPlugin).toBeDefined(); - }); -}); +export { Contexts } from './Contexts'; +export type { Context } from './types'; diff --git a/packages/backend-common/src/context/types.ts b/packages/backend-common/src/context/types.ts new file mode 100644 index 0000000000..664745bbf6 --- /dev/null +++ b/packages/backend-common/src/context/types.ts @@ -0,0 +1,45 @@ +/* + * 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 { AbortSignal } from 'node-abort-controller'; + +/** + * A context that is meant to be passed as a ctx variable down the call chain, + * to pass along scoped information and abort signals. + * + * @alpha + */ +export interface Context { + /** + * Returns an abort signal that triggers when the current context or any of + * its parents signal for it. + */ + readonly abortSignal: AbortSignal; + + /** + * The point in time when the current context shall time out and abort, if + * applicable. + */ + readonly deadline: Date | undefined; + + /** + * Attempts to get a stored value by key from the context. + * + * @param key - The key of the value to get + * @returns The associated value, or undefined if not set + */ + value(key: string): T | undefined; +} diff --git a/packages/backend-common/src/database/util.ts b/packages/backend-common/src/database/util.ts index 607554f294..a96eeb3a7b 100644 --- a/packages/backend-common/src/database/util.ts +++ b/packages/backend-common/src/database/util.ts @@ -27,7 +27,7 @@ export function isDatabaseConflictError(e: unknown) { return ( typeof message === 'string' && - (/SQLITE_CONSTRAINT: UNIQUE/.test(message) || + (/SQLITE_CONSTRAINT(?:_UNIQUE)?: UNIQUE/.test(message) || /unique constraint/.test(message)) ); } diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index e430ddc1a4..238e58b2a8 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -22,6 +22,7 @@ export * from './cache'; export { loadBackendConfig } from './config'; +export * from './context'; export * from './database'; export * from './discovery'; export * from './hot'; diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index cfaf40a6d6..d611ee8ac1 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/backend-tasks +## 0.1.6 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7 + +## 0.1.6-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + ## 0.1.4 ### Patch Changes diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 45a404f457..364f1d1380 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.1.4", + "version": "0.1.6", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", "@backstage/types": "^0.1.1", "@types/luxon": "^2.0.4", - "knex": "^0.95.1", + "knex": "^1.0.2", "lodash": "^4.17.21", "luxon": "^2.0.2", "node-abort-controller": "^3.0.1", @@ -43,8 +43,8 @@ "zod": "^3.9.5" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.15", - "@backstage/cli": "^0.13.0", + "@backstage/backend-test-utils": "^0.1.17", + "@backstage/cli": "^0.13.2", "jest": "^26.0.1", "wait-for-expect": "^3.0.2" }, diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 0b3fc1f5b2..55fb3c94cd 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/backend-test-utils +## 0.1.17 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/cli@0.13.2 + - @backstage/backend-common@0.10.7 + +## 0.1.17-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/cli@0.13.2-next.0 + - @backstage/backend-common@0.10.7-next.0 + +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.13.1 + - @backstage/backend-common@0.10.6 + +## 0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.13.1-next.1 + - @backstage/backend-common@0.10.6-next.0 + +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.13.1-next.0 + ## 0.1.15 ### Patch Changes diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index dbbc122fc9..8a9767b292 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.15", + "version": "0.1.17", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,18 +30,18 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/cli": "^0.13.0", + "@backstage/backend-common": "^0.10.7", + "@backstage/cli": "^0.13.2", "@backstage/config": "^0.1.13", - "knex": "^0.95.1", + "@vscode/sqlite3": "^5.0.7", + "knex": "^1.0.2", "mysql2": "^2.2.5", "pg": "^8.3.0", - "sqlite3": "^5.0.1", "testcontainers": "^8.1.2", "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "jest": "^26.0.1" }, "files": [ diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 10962ab2bc..d68ec80469 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,151 @@ # example-backend +## 0.2.64 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/plugin-auth-backend@0.10.0 + - @backstage/backend-common@0.10.7 + - @backstage/backend-tasks@0.1.6 + - @backstage/plugin-app-backend@0.3.24 + - @backstage/plugin-catalog-backend@0.21.3 + - @backstage/plugin-code-coverage-backend@0.1.22 + - @backstage/plugin-scaffolder-backend@0.15.24 + - @backstage/plugin-search-backend-module-pg@0.2.6 + - @backstage/plugin-tech-insights-backend@0.2.4 + - @backstage/plugin-techdocs-backend@0.13.3 + - @backstage/plugin-auth-node@0.1.0 + - @backstage/plugin-permission-backend@0.4.3 + - @backstage/plugin-search-backend@0.4.2 + - @backstage/plugin-badges-backend@0.1.18 + - @backstage/plugin-jenkins-backend@0.1.13 + - @backstage/plugin-todo-backend@0.1.21 + - @backstage/plugin-permission-node@0.4.3 + - example-app@0.2.64 + - @backstage/plugin-azure-devops-backend@0.3.3 + - @backstage/plugin-graphql-backend@0.1.14 + - @backstage/plugin-kafka-backend@0.2.17 + - @backstage/plugin-kubernetes-backend@0.4.7 + - @backstage/plugin-proxy-backend@0.2.18 + - @backstage/plugin-rollbar-backend@0.1.21 + - @backstage/plugin-scaffolder-backend-module-rails@0.2.6 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.8 + - @backstage/plugin-tech-insights-node@0.2.2 + +## 0.2.64-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.10.0-next.0 + - @backstage/backend-common@0.10.7-next.0 + - @backstage/backend-tasks@0.1.6-next.0 + - @backstage/plugin-app-backend@0.3.24-next.0 + - @backstage/plugin-catalog-backend@0.21.3-next.0 + - @backstage/plugin-code-coverage-backend@0.1.22-next.0 + - @backstage/plugin-scaffolder-backend@0.15.24-next.0 + - @backstage/plugin-search-backend-module-pg@0.2.6-next.0 + - @backstage/plugin-tech-insights-backend@0.2.4-next.0 + - @backstage/plugin-techdocs-backend@0.13.3-next.0 + - example-app@0.2.64-next.0 + - @backstage/plugin-azure-devops-backend@0.3.3-next.0 + - @backstage/plugin-badges-backend@0.1.18-next.0 + - @backstage/plugin-graphql-backend@0.1.14-next.0 + - @backstage/plugin-jenkins-backend@0.1.13-next.0 + - @backstage/plugin-kafka-backend@0.2.17-next.0 + - @backstage/plugin-kubernetes-backend@0.4.7-next.0 + - @backstage/plugin-permission-backend@0.4.3-next.0 + - @backstage/plugin-permission-node@0.4.3-next.0 + - @backstage/plugin-proxy-backend@0.2.18-next.0 + - @backstage/plugin-rollbar-backend@0.1.21-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.2.6-next.0 + - @backstage/plugin-search-backend@0.4.2-next.0 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.8-next.0 + - @backstage/plugin-tech-insights-node@0.2.2-next.0 + - @backstage/plugin-todo-backend@0.1.21-next.0 + +## 0.2.63 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0 + - @backstage/plugin-rollbar-backend@0.1.20 + - @backstage/plugin-catalog-backend@0.21.2 + - @backstage/plugin-scaffolder-backend@0.15.23 + - @backstage/plugin-proxy-backend@0.2.17 + - @backstage/backend-common@0.10.6 + - example-app@0.2.63 + - @backstage/backend-tasks@0.1.5 + - @backstage/plugin-app-backend@0.3.23 + - @backstage/plugin-azure-devops-backend@0.3.2 + - @backstage/plugin-badges-backend@0.1.17 + - @backstage/plugin-code-coverage-backend@0.1.21 + - @backstage/plugin-graphql-backend@0.1.13 + - @backstage/plugin-jenkins-backend@0.1.12 + - @backstage/plugin-kafka-backend@0.2.16 + - @backstage/plugin-kubernetes-backend@0.4.6 + - @backstage/plugin-permission-backend@0.4.2 + - @backstage/plugin-permission-node@0.4.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.2.5 + - @backstage/plugin-search-backend@0.4.1 + - @backstage/plugin-search-backend-module-pg@0.2.5 + - @backstage/plugin-tech-insights-backend@0.2.3 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.7 + - @backstage/plugin-tech-insights-node@0.2.1 + - @backstage/plugin-techdocs-backend@0.13.2 + - @backstage/plugin-todo-backend@0.1.20 + +## 0.2.63-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0-next.1 + - @backstage/backend-common@0.10.6-next.0 + - example-app@0.2.63-next.1 + - @backstage/plugin-catalog-backend@0.21.2-next.1 + - @backstage/plugin-techdocs-backend@0.13.2-next.0 + - @backstage/backend-tasks@0.1.5-next.0 + - @backstage/plugin-app-backend@0.3.23-next.0 + - @backstage/plugin-azure-devops-backend@0.3.2-next.0 + - @backstage/plugin-badges-backend@0.1.17-next.0 + - @backstage/plugin-code-coverage-backend@0.1.21-next.0 + - @backstage/plugin-graphql-backend@0.1.13-next.0 + - @backstage/plugin-jenkins-backend@0.1.12-next.0 + - @backstage/plugin-kafka-backend@0.2.16-next.0 + - @backstage/plugin-kubernetes-backend@0.4.6-next.0 + - @backstage/plugin-permission-backend@0.4.2-next.1 + - @backstage/plugin-permission-node@0.4.2-next.1 + - @backstage/plugin-proxy-backend@0.2.17-next.1 + - @backstage/plugin-rollbar-backend@0.1.20-next.1 + - @backstage/plugin-scaffolder-backend@0.15.23-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.2.5-next.1 + - @backstage/plugin-search-backend@0.4.1-next.1 + - @backstage/plugin-search-backend-module-pg@0.2.5-next.0 + - @backstage/plugin-tech-insights-backend@0.2.3-next.0 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.7-next.0 + - @backstage/plugin-tech-insights-node@0.2.1-next.0 + - @backstage/plugin-todo-backend@0.1.20-next.0 + +## 0.2.63-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0-next.0 + - @backstage/plugin-rollbar-backend@0.1.20-next.0 + - @backstage/plugin-catalog-backend@0.21.2-next.0 + - @backstage/plugin-scaffolder-backend@0.15.23-next.0 + - @backstage/plugin-proxy-backend@0.2.17-next.0 + - @backstage/plugin-permission-backend@0.4.2-next.0 + - @backstage/plugin-permission-node@0.4.2-next.0 + - @backstage/plugin-search-backend@0.4.1-next.0 + - example-app@0.2.63-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.2.5-next.0 + ## 0.2.62 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 44544703d8..35b234aeda 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.62", + "version": "0.2.64", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,55 +24,56 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/backend-tasks": "^0.1.4", - "@backstage/catalog-client": "^0.5.5", + "@backstage/backend-common": "^0.10.7", + "@backstage/backend-tasks": "^0.1.6", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/integration": "^0.7.2", - "@backstage/plugin-app-backend": "^0.3.22", - "@backstage/plugin-auth-backend": "^0.8.0", - "@backstage/plugin-azure-devops-backend": "^0.3.1", - "@backstage/plugin-badges-backend": "^0.1.16", - "@backstage/plugin-catalog-backend": "^0.21.1", - "@backstage/plugin-code-coverage-backend": "^0.1.20", - "@backstage/plugin-graphql-backend": "^0.1.12", - "@backstage/plugin-jenkins-backend": "^0.1.11", - "@backstage/plugin-kubernetes-backend": "^0.4.5", - "@backstage/plugin-kafka-backend": "^0.2.15", - "@backstage/plugin-permission-backend": "^0.4.1", + "@backstage/plugin-app-backend": "^0.3.24", + "@backstage/plugin-auth-backend": "^0.10.0", + "@backstage/plugin-auth-node": "^0.1.0", + "@backstage/plugin-azure-devops-backend": "^0.3.3", + "@backstage/plugin-badges-backend": "^0.1.18", + "@backstage/plugin-catalog-backend": "^0.21.3", + "@backstage/plugin-code-coverage-backend": "^0.1.22", + "@backstage/plugin-graphql-backend": "^0.1.14", + "@backstage/plugin-jenkins-backend": "^0.1.13", + "@backstage/plugin-kubernetes-backend": "^0.4.7", + "@backstage/plugin-kafka-backend": "^0.2.17", + "@backstage/plugin-permission-backend": "^0.4.3", "@backstage/plugin-permission-common": "^0.4.0", - "@backstage/plugin-permission-node": "^0.4.1", - "@backstage/plugin-proxy-backend": "^0.2.16", - "@backstage/plugin-rollbar-backend": "^0.1.19", - "@backstage/plugin-scaffolder-backend": "^0.15.22", - "@backstage/plugin-scaffolder-backend-module-rails": "^0.2.4", - "@backstage/plugin-search-backend": "^0.4.0", + "@backstage/plugin-permission-node": "^0.4.3", + "@backstage/plugin-proxy-backend": "^0.2.18", + "@backstage/plugin-rollbar-backend": "^0.1.21", + "@backstage/plugin-scaffolder-backend": "^0.15.24", + "@backstage/plugin-scaffolder-backend-module-rails": "^0.2.6", + "@backstage/plugin-search-backend": "^0.4.2", "@backstage/plugin-search-backend-node": "^0.4.5", "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.8", - "@backstage/plugin-search-backend-module-pg": "^0.2.4", - "@backstage/plugin-techdocs-backend": "^0.13.1", - "@backstage/plugin-tech-insights-backend": "^0.2.2", - "@backstage/plugin-tech-insights-node": "^0.2.0", - "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.6", - "@backstage/plugin-todo-backend": "^0.1.19", + "@backstage/plugin-search-backend-module-pg": "^0.2.6", + "@backstage/plugin-techdocs-backend": "^0.13.3", + "@backstage/plugin-tech-insights-backend": "^0.2.4", + "@backstage/plugin-tech-insights-node": "^0.2.2", + "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.8", + "@backstage/plugin-todo-backend": "^0.1.21", "@gitbeaker/node": "^35.1.0", "@octokit/rest": "^18.5.3", + "@vscode/sqlite3": "^5.0.7", "azure-devops-node-api": "^11.0.1", "dockerode": "^3.3.1", - "example-app": "file:../app", + "example-app": "link:../app", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-prom-bundle": "^6.3.6", - "knex": "^0.95.1", + "knex": "^1.0.2", "pg": "^8.3.0", "pg-connection-string": "^2.3.0", "prom-client": "^14.0.1", - "sqlite3": "^5.0.1", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/dockerode": "^3.3.0", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/plugins/permission.ts b/packages/backend/src/plugins/permission.ts index 71a9b90311..557cea9a26 100644 --- a/packages/backend/src/plugins/permission.ts +++ b/packages/backend/src/plugins/permission.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { IdentityClient } from '@backstage/plugin-auth-node'; import { createRouter } from '@backstage/plugin-permission-backend'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { @@ -35,12 +35,13 @@ class AllowAllPermissionPolicy implements PermissionPolicy { export default async function createPlugin( env: PluginEnvironment, ): Promise { - const { logger, discovery } = env; + const { logger, discovery, config } = env; return await createRouter({ + config, logger, discovery, policy: new AllowAllPermissionPolicy(), - identity: new IdentityClient({ + identity: IdentityClient.create({ discovery, issuer: await discovery.getExternalBaseUrl('auth'), }), diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index 81bc155bbc..664e7a49ed 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/catalog-client +## 0.6.0 + +### Minor Changes + +- f8633307c4: Fixed the return type of the catalog API `getEntityAncestors`, to match the + actual server response shape. + + While this technically is a breaking change, the old shape has never worked at + all if you tried to use it - so treating this as an immediately-shipped breaking + bug fix. + ## 0.5.5 ### Patch Changes diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index f3921993d3..090a1dbcc4 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -5,7 +5,7 @@ ```ts import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; -import { Location as Location_2 } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/catalog-model'; // @public export type AddLocationRequest = { @@ -32,13 +32,13 @@ export interface CatalogApi { options?: CatalogRequestOptions, ): Promise; getEntities( - request?: CatalogEntitiesRequest, + request?: GetEntitiesRequest, options?: CatalogRequestOptions, - ): Promise>; + ): Promise; getEntityAncestors( - request: CatalogEntityAncestorsRequest, + request: GetEntityAncestorsRequest, options?: CatalogRequestOptions, - ): Promise; + ): Promise; getEntityByName( name: EntityName, options?: CatalogRequestOptions, @@ -71,19 +71,26 @@ export interface CatalogApi { // @public export class CatalogClient implements CatalogApi { - constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }); + constructor(options: { + discoveryApi: { + getBaseUrl(pluginId: string): Promise; + }; + fetchApi?: { + fetch: typeof fetch; + }; + }); addLocation( { type, target, dryRun, presence }: AddLocationRequest, options?: CatalogRequestOptions, ): Promise; getEntities( - request?: CatalogEntitiesRequest, + request?: GetEntitiesRequest, options?: CatalogRequestOptions, - ): Promise>; + ): Promise; getEntityAncestors( - request: CatalogEntityAncestorsRequest, + request: GetEntityAncestorsRequest, options?: CatalogRequestOptions, - ): Promise; + ): Promise; getEntityByName( compoundName: EntityName, options?: CatalogRequestOptions, @@ -114,53 +121,66 @@ export class CatalogClient implements CatalogApi { ): Promise; } -// @public -export type CatalogEntitiesRequest = { - filter?: - | Record[] - | Record - | undefined; - fields?: string[] | undefined; - offset?: number; - limit?: number; - after?: string; -}; +// @public @deprecated (undocumented) +export type CatalogEntitiesRequest = GetEntitiesRequest; + +// @public @deprecated (undocumented) +export type CatalogEntityAncestorsRequest = GetEntityAncestorsRequest; + +// @public @deprecated (undocumented) +export type CatalogEntityAncestorsResponse = GetEntityAncestorsResponse; + +// @public @deprecated (undocumented) +export type CatalogListResponse<_Entity> = GetEntitiesResponse; // @public -export type CatalogEntityAncestorsRequest = { - entityRef: string; -}; - -// @public -export type CatalogEntityAncestorsResponse = { - root: EntityName; - items: { - entity: Entity; - parents: EntityName[]; - }[]; -}; - -// @public -export type CatalogListResponse = { - items: T[]; -}; - -// @public -export type CatalogRequestOptions = { +export interface CatalogRequestOptions { + // (undocumented) token?: string; -}; - -// @public -export type DiscoveryApi = { - getBaseUrl(pluginId: string): Promise; -}; +} // @public export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = 'backstage.io/catalog-processing'; // @public -export type FetchApi = { - fetch: typeof fetch; -}; +export interface GetEntitiesRequest { + after?: string; + fields?: string[] | undefined; + filter?: + | Record[] + | Record + | undefined; + limit?: number; + offset?: number; +} + +// @public +export interface GetEntitiesResponse { + // (undocumented) + items: Entity[]; +} + +// @public +export interface GetEntityAncestorsRequest { + // (undocumented) + entityRef: string; +} + +// @public +export interface GetEntityAncestorsResponse { + // (undocumented) + items: Array<{ + entity: Entity; + parentEntityRefs: string[]; + }>; + // (undocumented) + rootEntityRef: string; +} + +// @public +type Location_2 = { + id: string; +} & LocationSpec; +export { Location_2 as Location }; ``` diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 543c107e65..3a45a74680 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-client", "description": "An isomorphic client for the catalog backend", - "version": "0.5.5", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "dependencies": { "@backstage/catalog-model": "^0.9.10", "@backstage/errors": "^0.2.0", - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/jest": "^26.0.7", "msw": "^0.35.0" }, diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index 1331f8e874..0035691f2c 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -18,7 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { CatalogClient } from './CatalogClient'; -import { CATALOG_FILTER_EXISTS, CatalogListResponse } from './types/api'; +import { CATALOG_FILTER_EXISTS, GetEntitiesResponse } from './types/api'; import { DiscoveryApi } from './types/discovery'; const server = setupServer(); @@ -60,7 +60,7 @@ describe('CatalogClient', () => { }, }, ]; - const defaultResponse: CatalogListResponse = { + const defaultResponse: GetEntitiesResponse = { items: defaultServiceResponse.reverse(), }; diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index a446d249c0..bcf8750658 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -15,14 +15,13 @@ */ import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, Entity, EntityName, - Location, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, parseEntityRef, stringifyEntityRef, - stringifyLocationReference, + stringifyLocationRef, } from '@backstage/catalog-model'; import { ResponseError } from '@backstage/errors'; import crossFetch from 'cross-fetch'; @@ -31,43 +30,41 @@ import { AddLocationRequest, AddLocationResponse, CatalogApi, - CatalogEntitiesRequest, - CatalogListResponse, + GetEntitiesRequest, + GetEntitiesResponse, CatalogRequestOptions, - CatalogEntityAncestorsRequest, - CatalogEntityAncestorsResponse, + GetEntityAncestorsRequest, + GetEntityAncestorsResponse, + Location, } from './types/api'; import { DiscoveryApi } from './types/discovery'; import { FetchApi } from './types/fetch'; /** - * A frontend and backend compatible client for communicating with the Backstage Catalog. + * A frontend and backend compatible client for communicating with the Backstage + * software catalog. * * @public - * */ + */ export class CatalogClient implements CatalogApi { private readonly discoveryApi: DiscoveryApi; private readonly fetchApi: FetchApi; - constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }) { + constructor(options: { + discoveryApi: { getBaseUrl(pluginId: string): Promise }; + fetchApi?: { fetch: typeof fetch }; + }) { this.discoveryApi = options.discoveryApi; this.fetchApi = options.fetchApi || { fetch: crossFetch }; } /** - * Gets the Ancestors of an Entity. - * - * @param request - A request type for retrieving Entity ancestors. - * @param options - An object with your preferred options. - * - * @returns A CatalogEntityAncestorsResponse. - * - * @public + * {@inheritdoc CatalogApi.getEntityAncestors} */ async getEntityAncestors( - request: CatalogEntityAncestorsRequest, + request: GetEntityAncestorsRequest, options?: CatalogRequestOptions, - ): Promise { + ): Promise { const { kind, namespace, name } = parseEntityRef(request.entityRef); return await this.requestRequired( 'GET', @@ -79,14 +76,7 @@ export class CatalogClient implements CatalogApi { } /** - * Gets a Location by Id. - * - * @param id - A string containing the Id. - * @param options - An object with your preferred options. - * - * @returns A {@link catalog-model#Location_2}. - * - * @public + * {@inheritdoc CatalogApi.getLocationById} */ async getLocationById( id: string, @@ -100,19 +90,12 @@ export class CatalogClient implements CatalogApi { } /** - * Gets a set of Entities. - * - * @param request - A request type for retrieving an Entity. - * @param options - An object with your preferred options. - * - * @returns A CatalogListResponse. - * - * @public + * {@inheritdoc CatalogApi.getEntities} */ async getEntities( - request?: CatalogEntitiesRequest, + request?: GetEntitiesRequest, options?: CatalogRequestOptions, - ): Promise> { + ): Promise { const { filter = [], fields = [], offset, limit, after } = request ?? {}; const filterItems = [filter].flat(); const params: string[] = []; @@ -187,14 +170,7 @@ export class CatalogClient implements CatalogApi { } /** - * Gets a given Entity based on a provided name. - * - * @param compoundName - A string containing the name. - * @param options - An object with your preferred options. - * - * @returns An {@link catalog-model#Entity}. - * - * @public + * {@inheritdoc CatalogApi.getEntityByName} */ async getEntityByName( compoundName: EntityName, @@ -211,12 +187,7 @@ export class CatalogClient implements CatalogApi { } /** - * Refreshes an Entity. - * - * @param entityRef - A string containing the entityREf - * @param options - An object with your preferred options. - * - * @public + * {@inheritdoc CatalogApi.refreshEntity} */ async refreshEntity(entityRef: string, options?: CatalogRequestOptions) { const response = await this.fetchApi.fetch( @@ -237,14 +208,7 @@ export class CatalogClient implements CatalogApi { } /** - * Adds a location. - * - * @param options - An object with your preferred options. - * @param AddLocationRequest - A request object for adding locations. - * - * @returns An AddLocationResponse - * - * @public + * {@inheritdoc CatalogApi.addLocation} */ async addLocation( { type = 'url', target, dryRun, presence }: AddLocationRequest, @@ -282,21 +246,14 @@ export class CatalogClient implements CatalogApi { } /** - * Gets an origin Location By Entity. - * - * @param entity - An Entity - * @param options - An object with your preferred options. - * - * @returns A {@link catalog-model#Location_2}. - * - * @public + * {@inheritdoc CatalogApi.getOriginLocationByEntity} */ async getOriginLocationByEntity( entity: Entity, options?: CatalogRequestOptions, ): Promise { const locationCompound = - entity.metadata.annotations?.[ORIGIN_LOCATION_ANNOTATION]; + entity.metadata.annotations?.[ANNOTATION_ORIGIN_LOCATION]; if (!locationCompound) { return undefined; } @@ -307,24 +264,17 @@ export class CatalogClient implements CatalogApi { ); return all .map(r => r.data) - .find(l => locationCompound === stringifyLocationReference(l)); + .find(l => locationCompound === stringifyLocationRef(l)); } /** - * Gets a Location by Entity. - * - * @param entity - An Entity - * @param options - An object with your preferred options. - * - * @returns A {@link catalog-model#Location_2}. - * - * @public + * {@inheritdoc CatalogApi.getLocationByEntity} */ async getLocationByEntity( entity: Entity, options?: CatalogRequestOptions, ): Promise { - const locationCompound = entity.metadata.annotations?.[LOCATION_ANNOTATION]; + const locationCompound = entity.metadata.annotations?.[ANNOTATION_LOCATION]; if (!locationCompound) { return undefined; } @@ -335,16 +285,11 @@ export class CatalogClient implements CatalogApi { ); return all .map(r => r.data) - .find(l => locationCompound === stringifyLocationReference(l)); + .find(l => locationCompound === stringifyLocationRef(l)); } /** - * Removes a location as identified by Id. - * - * @param id - A string containing the Id - * @param options - An object with your preferred options. - * - * @public + * {@inheritdoc CatalogApi.removeLocationById} */ async removeLocationById( id: string, @@ -358,12 +303,7 @@ export class CatalogClient implements CatalogApi { } /** - * Removes an Entity as identified by Uid. - * - * @param uid - A string containing the Uid - * @param options - An object with your preferred options. - * - * @public + * {@inheritdoc CatalogApi.removeEntityByUid} */ async removeEntityByUid( uid: string, diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 3a09e14d71..bf5a7327c6 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -14,21 +14,26 @@ * limitations under the License. */ -import { Entity, EntityName, Location } from '@backstage/catalog-model'; +import { Entity, EntityName, LocationSpec } from '@backstage/catalog-model'; /** - * A Symbol to define if a catalog filter exists or not. + * This symbol can be used in place of a value when passed to filters in e.g. + * {@link CatalogClient.getEntities}, to signify that you want to filter on the + * presence of that key no matter what its value is. * * @public */ -export const CATALOG_FILTER_EXISTS = Symbol('CATALOG_FILTER_EXISTS'); +export const CATALOG_FILTER_EXISTS = Symbol.for( + // Random UUID to ensure no collisions + 'CATALOG_FILTER_EXISTS_0e15b590c0b343a2bae3e787e84c2111', +); /** - * A request type for retrieving catalog Entities. + * The request type for {@link CatalogClient.getEntities}. * * @public */ -export type CatalogEntitiesRequest = { +export interface GetEntitiesRequest { /** * If given, return only entities that match the given patterns. * @@ -104,175 +109,59 @@ export type CatalogEntitiesRequest = { * request. */ after?: string; -}; +} /** - * A request type for Catalog Entity Ancestor information. + * The response type for {@link CatalogClient.getEntities}. * * @public */ -export type CatalogEntityAncestorsRequest = { +export interface GetEntitiesResponse { + items: Entity[]; +} + +/** + * The request type for {@link CatalogClient.getEntityAncestors}. + * + * @public + */ +export interface GetEntityAncestorsRequest { entityRef: string; -}; +} /** - * A response type for Catalog Entity Ancestor information. + * The response type for {@link CatalogClient.getEntityAncestors}. * * @public */ -export type CatalogEntityAncestorsResponse = { - root: EntityName; - items: { entity: Entity; parents: EntityName[] }[]; -}; - -/** - * A response type for the result of a catalog operation in list form. - * - * @public - */ -export type CatalogListResponse = { - items: T[]; -}; +export interface GetEntityAncestorsResponse { + rootEntityRef: string; + items: Array<{ + entity: Entity; + parentEntityRefs: string[]; + }>; +} /** * Options you can pass into a catalog request for additional information. * * @public */ -export type CatalogRequestOptions = { +export interface CatalogRequestOptions { token?: string; -}; - -/** - * Public functions for interacting with the Catalog API. - * - * @public - */ -export interface CatalogApi { - /** - * Gets the Entities from the catalog based on your request and options. - * - * @param request - An object with your filters and fields. - * @param options - An object with your preferred options. - * - * @returns A CatalogListResponse with items typed Catalog Model Entity. - * - */ - getEntities( - request?: CatalogEntitiesRequest, - options?: CatalogRequestOptions, - ): Promise>; - /** - * Gets the Entity ancestor information from the catalog based on your request and options. - * - * @param request - An object with your filters and fields. - * @param options - An object with your preferred options. - * - * @returns A CatalogEntityAncestorsResponse. - */ - getEntityAncestors( - request: CatalogEntityAncestorsRequest, - options?: CatalogRequestOptions, - ): Promise; - /** - * Gets a single Entity from the catalog by Entity name. - * - * @param name - A complete Entity name, with the full kind-namespace-name triplet. - * @param options - An object with your preferred options. - * - * @returns A {@link catalog-model#Entity}. - */ - getEntityByName( - name: EntityName, - options?: CatalogRequestOptions, - ): Promise; - /** - * Removes a single Entity from the catalog by Entity UID. - * - * @param uid - A string of the Entity UID. - * @param options - An object with your preferred options. - * - */ - removeEntityByUid( - uid: string, - options?: CatalogRequestOptions, - ): Promise; - /** - * Refreshes an Entity in the catalog. - * - * @param entityRef - A string in the form of 'Kind/default:foo'. - * @param options - An object with your preferred options. - * - */ - refreshEntity( - entityRef: string, - options?: CatalogRequestOptions, - ): Promise; - - // Locations - /** - * Gets a Location object by ID from the catalog. - * - * @param id - A string in of the Location Id. - * @param options - An object with your preferred options. - * - * @returns A {@link catalog-model#Location_2}. - */ - getLocationById( - id: string, - options?: CatalogRequestOptions, - ): Promise; - /** - * Gets origin location by Entity. - * - * @param entity - An {@link catalog-model#Entity}. - * @param options - An object with your preferred options. - * - * @returns A {@link catalog-model#Location_2}. - */ - getOriginLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise; - /** - * Gets Location by Entity. - * - * @param entity - An {@link catalog-model#Entity}. - * @param options - An object with your preferred options. - * - * @returns A {@link catalog-model#Location_2}. - */ - getLocationByEntity( - entity: Entity, - options?: CatalogRequestOptions, - ): Promise; - /** - * Adds a Location. - * - * @param location - A request type for adding a Location to the catalog. - * @param options - An object with your preferred options. - * - * @returns A AddLocationResponse. - */ - addLocation( - location: AddLocationRequest, - options?: CatalogRequestOptions, - ): Promise; - /** - * Removes a Location by Id. - * - * @param id - A string in of the Location Id. - * @param options - An object with your preferred options. - * - */ - removeLocationById( - id: string, - options?: CatalogRequestOptions, - ): Promise; } /** - * A request type for adding a Location to the catalog. + * Entity location for a specific entity. + * + * @public + */ +export type Location = { + id: string; +} & LocationSpec; + +/** + * The request type for {@link CatalogClient.addLocation}. * * @public */ @@ -284,13 +173,135 @@ export type AddLocationRequest = { }; /** - * A response type for adding a Location to the catalog. + * The response type for {@link CatalogClient.addLocation}. * * @public */ export type AddLocationResponse = { location: Location; entities: Entity[]; - // Exists is only set in DryRun mode. + // Only set in dryRun mode. exists?: boolean; }; + +/** + * A client for interacting with the Backstage software catalog through its API. + * + * @public + */ +export interface CatalogApi { + /** + * Lists catalog entities. + * + * @param request - Request parameters + * @param options - Additional options + */ + getEntities( + request?: GetEntitiesRequest, + options?: CatalogRequestOptions, + ): Promise; + + /** + * Gets entity ancestor information, i.e. the hierarchy of parent entities + * whose processing resulted in a given entity appearing in the catalog. + * + * @param request - Request parameters + * @param options - Additional options + */ + getEntityAncestors( + request: GetEntityAncestorsRequest, + options?: CatalogRequestOptions, + ): Promise; + + /** + * Gets a single entity from the catalog by its ref (kind, namespace, name) + * triplet. + * + * @param name - A complete entity ref + * @param options - Additional options + */ + getEntityByName( + name: EntityName, + options?: CatalogRequestOptions, + ): Promise; + + /** + * Removes a single entity from the catalog by entity UID. + * + * @param uid - An entity UID + * @param options - Additional options + */ + removeEntityByUid( + uid: string, + options?: CatalogRequestOptions, + ): Promise; + + /** + * Refreshes (marks for reprocessing) an entity in the catalog. + * + * @param entityRef - An entity ref on string form (e.g. + * 'component/default:my-component') + * @param options - Additional options + */ + refreshEntity( + entityRef: string, + options?: CatalogRequestOptions, + ): Promise; + + // Locations + + /** + * Gets a registered location by its ID. + * + * @param id - A location ID + * @param options - Additional options + */ + getLocationById( + id: string, + options?: CatalogRequestOptions, + ): Promise; + + /** + * Gets origin location by Entity. + * + * @param entity - An {@link catalog-model#Entity}. + * @param options - Additional options + */ + getOriginLocationByEntity( + entity: Entity, + options?: CatalogRequestOptions, + ): Promise; + + /** + * Gets Location by Entity. + * + * @param entity - An {@link catalog-model#Entity}. + * @param options - Additional options + */ + getLocationByEntity( + entity: Entity, + options?: CatalogRequestOptions, + ): Promise; + + /** + * Registers a new location. + * + * @param location - Request parameters + * @param options - Additional options + */ + addLocation( + location: AddLocationRequest, + options?: CatalogRequestOptions, + ): Promise; + + /** + * Removes a registered Location by its ID. + * + * @param id - A location ID + * @param options - Additional options + */ + removeLocationById( + id: string, + options?: CatalogRequestOptions, + ): Promise; +} diff --git a/packages/catalog-client/src/types/deprecated.ts b/packages/catalog-client/src/types/deprecated.ts new file mode 100644 index 0000000000..0b145f3024 --- /dev/null +++ b/packages/catalog-client/src/types/deprecated.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + GetEntitiesRequest, + GetEntitiesResponse, + GetEntityAncestorsRequest, + GetEntityAncestorsResponse, +} from './api'; + +/** + * @public + * @deprecated use GetEntitiesRequest instead + */ +export type CatalogEntitiesRequest = GetEntitiesRequest; +/** + * @public + * @deprecated use GetEntitiesResponse instead + */ +export type CatalogListResponse<_Entity> = GetEntitiesResponse; +/** + * @public + * @deprecated use GetEntityAncestorsRequest instead + */ +export type CatalogEntityAncestorsRequest = GetEntityAncestorsRequest; +/** + * @public + * @deprecated use GetEntityAncestorsResponse instead + */ +export type CatalogEntityAncestorsResponse = GetEntityAncestorsResponse; diff --git a/packages/catalog-client/src/types/discovery.ts b/packages/catalog-client/src/types/discovery.ts index 5fa14372e3..19f304b1fd 100644 --- a/packages/catalog-client/src/types/discovery.ts +++ b/packages/catalog-client/src/types/discovery.ts @@ -16,8 +16,6 @@ /** * This is a copy of the DiscoveryApi, to avoid importing core-plugin-api. - * - * @public */ export type DiscoveryApi = { getBaseUrl(pluginId: string): Promise; diff --git a/packages/catalog-client/src/types/fetch.ts b/packages/catalog-client/src/types/fetch.ts index 59de6a68f1..047a9f3558 100644 --- a/packages/catalog-client/src/types/fetch.ts +++ b/packages/catalog-client/src/types/fetch.ts @@ -16,8 +16,6 @@ /** * This is a copy of FetchApi, to avoid importing core-plugin-api. - * - * @public */ export type FetchApi = { fetch: typeof fetch; diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index f3c6fab4e9..580bb7330d 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -14,17 +14,17 @@ * limitations under the License. */ +export { CATALOG_FILTER_EXISTS } from './api'; export type { AddLocationRequest, AddLocationResponse, CatalogApi, - CatalogEntitiesRequest, - CatalogListResponse, CatalogRequestOptions, - CatalogEntityAncestorsRequest, - CatalogEntityAncestorsResponse, + GetEntitiesRequest, + GetEntitiesResponse, + GetEntityAncestorsRequest, + GetEntityAncestorsResponse, + Location, } from './api'; -export type { DiscoveryApi } from './discovery'; -export type { FetchApi } from './fetch'; -export { CATALOG_FILTER_EXISTS } from './api'; export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status'; +export * from './deprecated'; diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 59eeb98924..655d01fef1 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -7,17 +7,27 @@ import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; import { JsonValue } from '@backstage/types'; import { SerializedError } from '@backstage/errors'; -import * as yup from 'yup'; // @alpha export interface AlphaEntity extends Entity { status?: EntityStatus; } -// @public @deprecated -export const analyzeLocationSchema: yup.SchemaOf<{ - location: LocationSpec; -}>; +// @public +export const ANNOTATION_EDIT_URL = 'backstage.io/edit-url'; + +// @public +export const ANNOTATION_LOCATION = 'backstage.io/managed-by-location'; + +// @public +export const ANNOTATION_ORIGIN_LOCATION = + 'backstage.io/managed-by-origin-location'; + +// @public +export const ANNOTATION_SOURCE_LOCATION = 'backstage.io/source-location'; + +// @public +export const ANNOTATION_VIEW_URL = 'backstage.io/view-url'; // @public interface ApiEntityV1alpha1 extends Entity { @@ -111,7 +121,7 @@ export { DomainEntityV1alpha1 }; // @public export const domainEntityV1alpha1Validator: KindValidator; -// @public +// @public @deprecated export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; // @public @@ -126,7 +136,7 @@ export type Entity = { // @public export const ENTITY_DEFAULT_NAMESPACE = 'default'; -// @public +// @public @deprecated export const ENTITY_META_GENERATED_FIELDS: readonly [ 'uid', 'etag', @@ -148,7 +158,7 @@ export function entityEnvelopeSchemaValidator< T extends EntityEnvelope = EntityEnvelope, >(schema?: unknown): (data: unknown) => T; -// @public +// @public @deprecated export function entityHasChanges(previous: Entity, next: Entity): boolean; // @public @@ -252,13 +262,13 @@ export class FieldFormatEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public +// @public @deprecated export function generateEntityEtag(): string; -// @public +// @public @deprecated export function generateEntityUid(): string; -// @public +// @public @deprecated export function generateUpdatedEntity(previous: Entity, next: Entity): Entity; // @public @@ -325,13 +335,13 @@ export class KubernetesValidatorFunctions { static isValidObjectName(value: unknown): boolean; } -// @public +// @public @deprecated type Location_2 = { id: string; } & LocationSpec; export { Location_2 as Location }; -// @public +// @public @deprecated export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; // @public @@ -354,9 +364,6 @@ export { LocationEntityV1alpha1 }; // @public export const locationEntityV1alpha1Validator: KindValidator; -// @public @deprecated -export const locationSchema: yup.SchemaOf; - // @public export type LocationSpec = { type: string; @@ -364,9 +371,6 @@ export type LocationSpec = { presence?: 'optional' | 'required'; }; -// @public @deprecated -export const locationSpecSchema: yup.SchemaOf; - // @public export function makeValidator(overrides?: Partial): Validators; @@ -377,7 +381,7 @@ export class NoForeignRootFieldsEntityPolicy implements EntityPolicy { enforce(entity: Entity): Promise; } -// @public +// @public @deprecated export const ORIGIN_LOCATION_ANNOTATION = 'backstage.io/managed-by-origin-location'; @@ -421,6 +425,12 @@ export function parseEntityRef( }; // @public +export function parseLocationRef(ref: string): { + type: string; + target: string; +}; + +// @public @deprecated export function parseLocationReference(ref: string): { type: string; target: string; @@ -479,6 +489,7 @@ interface ResourceEntityV1alpha1 extends Entity { type: string; owner: string; dependsOn?: string[]; + dependencyOf?: string[]; system?: string; }; } @@ -495,17 +506,6 @@ export class SchemaValidEntityPolicy implements EntityPolicy { } // @public @deprecated -export function serializeEntityRef( - ref: - | Entity - | { - kind?: string; - namespace?: string; - name: string; - }, -): EntityRef; - -// @public export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; // @public @@ -520,6 +520,12 @@ export function stringifyEntityRef( ): string; // @public +export function stringifyLocationRef(ref: { + type: string; + target: string; +}): string; + +// @public @deprecated export function stringifyLocationReference(ref: { type: string; target: string; @@ -605,6 +611,6 @@ export type Validators = { isValidTag(value: unknown): boolean; }; -// @public +// @public @deprecated export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; ``` diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 2d9218ec0b..62e577df4a 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -35,15 +35,13 @@ "@backstage/errors": "^0.2.0", "@backstage/types": "^0.1.1", "@types/json-schema": "^7.0.5", - "@types/yup": "^0.29.13", "ajv": "^7.0.3", "json-schema": "^0.4.0", "lodash": "^4.17.21", - "uuid": "^8.0.0", - "yup": "^0.32.9" + "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2-next.0", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" diff --git a/packages/catalog-model/src/entity/constants.ts b/packages/catalog-model/src/entity/constants.ts index 6daf6b78d9..4d0e6799be 100644 --- a/packages/catalog-model/src/entity/constants.ts +++ b/packages/catalog-model/src/entity/constants.ts @@ -25,6 +25,7 @@ export const ENTITY_DEFAULT_NAMESPACE = 'default'; * The keys of EntityMeta that are auto-generated. * * @public + * @deprecated will be removed in a future release. */ export const ENTITY_META_GENERATED_FIELDS = [ 'uid', @@ -36,6 +37,7 @@ export const ENTITY_META_GENERATED_FIELDS = [ * Annotation for linking to entity page from catalog pages. * * @public + * @deprecated use {@link ANNOTATION_VIEW_URL} instead. */ export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; @@ -43,5 +45,20 @@ export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; * Annotation for linking to entity edit page from catalog pages. * * @public + * @deprecated use {@link ANNOTATION_EDIT_URL} instead. */ export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; + +/** + * Annotation for linking to entity page from catalog pages. + * + * @public + */ +export const ANNOTATION_VIEW_URL = 'backstage.io/view-url'; + +/** + * Annotation for linking to entity edit page from catalog pages. + * + * @public + */ +export const ANNOTATION_EDIT_URL = 'backstage.io/edit-url'; diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index ce1e76a20b..49a6da5e0a 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -19,6 +19,8 @@ export { ENTITY_DEFAULT_NAMESPACE, ENTITY_META_GENERATED_FIELDS, VIEW_URL_ANNOTATION, + ANNOTATION_EDIT_URL, + ANNOTATION_VIEW_URL, } from './constants'; export type { AlphaEntity, @@ -40,7 +42,6 @@ export { getEntityName, parseEntityName, parseEntityRef, - serializeEntityRef, stringifyEntityRef, } from './ref'; export type { EntityRefContext } from './ref'; diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts index cf4bf0d869..f80b588d55 100644 --- a/packages/catalog-model/src/entity/ref.test.ts +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -16,12 +16,7 @@ import { ENTITY_DEFAULT_NAMESPACE } from './constants'; import { Entity } from './Entity'; -import { - compareEntityToRef, - parseEntityName, - parseEntityRef, - serializeEntityRef, -} from './ref'; +import { compareEntityToRef, parseEntityName, parseEntityRef } from './ref'; describe('ref', () => { describe('parseEntityName', () => { @@ -329,64 +324,6 @@ describe('ref', () => { }); }); - describe('serializeEntityRef', () => { - it('handles partials', () => { - expect( - serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c' }), - ).toEqual('a:b/c'); - expect(serializeEntityRef({ namespace: 'b', name: 'c' })).toEqual('b/c'); - expect(serializeEntityRef({ kind: 'a', name: 'c' })).toEqual('a:c'); - expect(serializeEntityRef({ name: 'c' })).toEqual('c'); - }); - - it('handles entities', () => { - const entityWithNamespace: Entity = { - apiVersion: 'a', - kind: 'b', - metadata: { - name: 'c', - namespace: 'd', - }, - }; - const entityWithoutNamespace: Entity = { - apiVersion: 'a', - kind: 'b', - metadata: { - name: 'c', - }, - }; - expect(serializeEntityRef(entityWithNamespace)).toEqual('b:d/c'); - expect(serializeEntityRef(entityWithoutNamespace)).toEqual('b:c'); - }); - - it('picks the least complex form', () => { - expect( - serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c' }), - ).toEqual('a:b/c'); - expect(serializeEntityRef({ namespace: 'b', name: 'c' })).toEqual('b/c'); - expect(serializeEntityRef({ kind: 'a', name: 'c' })).toEqual('a:c'); - expect(serializeEntityRef({ name: 'c' })).toEqual('c'); - expect( - serializeEntityRef({ kind: 'a:x', namespace: 'b', name: 'c' }), - ).toEqual({ kind: 'a:x', namespace: 'b', name: 'c' }); - expect( - serializeEntityRef({ kind: 'a/x', namespace: 'b', name: 'c' }), - ).toEqual({ kind: 'a/x', namespace: 'b', name: 'c' }); - expect( - serializeEntityRef({ kind: 'a', namespace: 'b:x', name: 'c' }), - ).toEqual({ kind: 'a', namespace: 'b:x', name: 'c' }); - expect( - serializeEntityRef({ kind: 'a', namespace: 'b/x', name: 'c' }), - ).toEqual({ kind: 'a', namespace: 'b/x', name: 'c' }); - expect( - serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c:x' }), - ).toEqual({ kind: 'a', namespace: 'b', name: 'c:x' }); - expect( - serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c/x' }), - ).toEqual({ kind: 'a', namespace: 'b', name: 'c/x' }); - }); - }); - describe('compareEntityToRef', () => { const entityWithNamespace: Entity = { apiVersion: 'a', diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index 958f750e32..1996616077 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -184,54 +184,6 @@ export function parseEntityRef( }; } -/** - * Takes an entity reference or name, and outputs an entity reference on the - * most compact form possible. I.e. if the parts do not contain any - * special/reserved characters, it outputs the string form, otherwise it - * outputs the compound form. - * - * @public - * @deprecated Use `stringifyEntityRef` instead - * @param ref - The reference to serialize - * @returns The same reference on either string or compound form - */ -export function serializeEntityRef( - ref: - | Entity - | { - kind?: string; - namespace?: string; - name: string; - }, -): EntityRef { - let kind; - let namespace; - let name; - - if ('metadata' in ref) { - kind = ref.kind; - namespace = ref.metadata.namespace; - name = ref.metadata.name; - } else { - kind = ref.kind; - namespace = ref.namespace; - name = ref.name; - } - - if ( - kind?.includes(':') || - kind?.includes('/') || - namespace?.includes(':') || - namespace?.includes('/') || - name.includes(':') || - name.includes('/') - ) { - return { kind, namespace, name }; - } - - return `${kind ? `${kind}:` : ''}${namespace ? `${namespace}/` : ''}${name}`; -} - /** * Takes an entity or entity name/reference, and returns the string form of an * entity ref. diff --git a/packages/catalog-model/src/entity/util.ts b/packages/catalog-model/src/entity/util.ts index 4506a8d852..78359b45f4 100644 --- a/packages/catalog-model/src/entity/util.ts +++ b/packages/catalog-model/src/entity/util.ts @@ -23,6 +23,7 @@ import { Entity, AlphaEntity } from './Entity'; * * @public * @returns A string with enough randomness to uniquely identify an entity + * @deprecated use `uuidv4()` instead. */ export function generateEntityUid(): string { return uuidv4(); @@ -34,6 +35,7 @@ export function generateEntityUid(): string { * @public * @returns A string with enough randomness to uniquely identify an entity * revision + * @deprecated will be removed in a future release. */ export function generateEntityEtag(): string { return Buffer.from(uuidv4(), 'utf8').toString('base64').replace(/[^\w]/g, ''); @@ -55,6 +57,7 @@ export function generateEntityEtag(): string { * @public * @param previous - The old state of the entity * @param next - The new state of the entity + * @deprecated will be removed in a future release. */ export function entityHasChanges(previous: Entity, next: Entity): boolean { const e1 = lodash.cloneDeep(previous); @@ -109,6 +112,7 @@ export function entityHasChanges(previous: Entity, next: Entity): boolean { * @param previous - The old state of the entity * @param next - The new state of the entity * @returns An entity with the merged state of both + * @deprecated will be removed in a future release. */ export function generateUpdatedEntity(previous: Entity, next: Entity): Entity { const { uid, etag, generation } = previous.metadata; diff --git a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts index 0a360cb60b..a1d49dd0a6 100644 --- a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts @@ -34,6 +34,7 @@ export interface ResourceEntityV1alpha1 extends Entity { type: string; owner: string; dependsOn?: string[]; + dependencyOf?: string[]; system?: string; }; } diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 8eb393f555..3ff624360e 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -17,17 +17,41 @@ /** * Constant storing location annotation. * - * @public */ + * @public + * @deprecated use {@link ANNOTATION_LOCATION} instead. + * */ export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; /** * Constant storing origin location annotation * - * @public */ + * @public + * @deprecated use {@link ANNOTATION_ORIGIN_LOCATION} instead. + */ export const ORIGIN_LOCATION_ANNOTATION = 'backstage.io/managed-by-origin-location'; /** * Contant storing source location annotation * - * @public */ + * @public + * @deprecated use {@link ANNOTATION_SOURCE_LOCATION} instead. + * */ export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; + +/** + * Constant storing location annotation. + * + * @public */ +export const ANNOTATION_LOCATION = 'backstage.io/managed-by-location'; +/** + * Constant storing origin location annotation + * + * @public */ +export const ANNOTATION_ORIGIN_LOCATION = + 'backstage.io/managed-by-origin-location'; + +/** + * Contant storing source location annotation + * + * @public */ +export const ANNOTATION_SOURCE_LOCATION = 'backstage.io/source-location'; diff --git a/packages/catalog-model/src/location/helpers.test.ts b/packages/catalog-model/src/location/helpers.test.ts index 020f7be1a1..91a2137611 100644 --- a/packages/catalog-model/src/location/helpers.test.ts +++ b/packages/catalog-model/src/location/helpers.test.ts @@ -16,47 +16,47 @@ import { getEntitySourceLocation, - parseLocationReference, - stringifyLocationReference, + parseLocationRef, + stringifyLocationRef, } from './helpers'; -describe('parseLocationReference', () => { +describe('parseLocationRef', () => { it('works for the simple case', () => { - expect(parseLocationReference('url:https://www.google.com')).toEqual({ + expect(parseLocationRef('url:https://www.google.com')).toEqual({ type: 'url', target: 'https://www.google.com', }); }); it('rejects faulty inputs', () => { - expect(() => parseLocationReference(7 as any)).toThrow( - "Unable to parse location reference '7', unexpected argument number", + expect(() => parseLocationRef(7 as any)).toThrow( + "Unable to parse location ref '7', unexpected argument number", ); - expect(() => parseLocationReference('')).toThrow( - "Unable to parse location reference '', expected ':', e.g. 'url:https://host/path'", + expect(() => parseLocationRef('')).toThrow( + "Unable to parse location ref '', expected ':', e.g. 'url:https://host/path'", ); - expect(() => parseLocationReference('hello')).toThrow( - "Unable to parse location reference 'hello', expected ':', e.g. 'url:https://host/path'", + expect(() => parseLocationRef('hello')).toThrow( + "Unable to parse location ref 'hello', expected ':', e.g. 'url:https://host/path'", ); - expect(() => parseLocationReference(':hello')).toThrow( - "Unable to parse location reference ':hello', expected ':', e.g. 'url:https://host/path'", + expect(() => parseLocationRef(':hello')).toThrow( + "Unable to parse location ref ':hello', expected ':', e.g. 'url:https://host/path'", ); - expect(() => parseLocationReference('hello:')).toThrow( - "Unable to parse location reference 'hello:', expected ':', e.g. 'url:https://host/path'", + expect(() => parseLocationRef('hello:')).toThrow( + "Unable to parse location ref 'hello:', expected ':', e.g. 'url:https://host/path'", ); - expect(() => parseLocationReference('http://blah')).toThrow( - "Invalid location reference 'http://blah', please prefix it with 'url:', e.g. 'url:http://blah'", + expect(() => parseLocationRef('http://blah')).toThrow( + "Invalid location ref 'http://blah', please prefix it with 'url:', e.g. 'url:http://blah'", ); - expect(() => parseLocationReference('https://bleh')).toThrow( - "Invalid location reference 'https://bleh', please prefix it with 'url:', e.g. 'url:https://bleh'", + expect(() => parseLocationRef('https://bleh')).toThrow( + "Invalid location ref 'https://bleh', please prefix it with 'url:', e.g. 'url:https://bleh'", ); }); }); -describe('stringifyLocationReference', () => { +describe('stringifyLocationRef', () => { it('works for the simple case', () => { expect( - stringifyLocationReference({ + stringifyLocationRef({ type: 'url', target: 'https://www.google.com', }), @@ -64,12 +64,12 @@ describe('stringifyLocationReference', () => { }); it('rejects faulty inputs', () => { - expect(() => - stringifyLocationReference({ type: '', target: 'hello' }), - ).toThrow('Unable to stringify location reference, empty type'); - expect(() => - stringifyLocationReference({ type: 'hello', target: '' }), - ).toThrow('Unable to stringify location reference, empty target'); + expect(() => stringifyLocationRef({ type: '', target: 'hello' })).toThrow( + 'Unable to stringify location ref, empty type', + ); + expect(() => stringifyLocationRef({ type: 'hello', target: '' })).toThrow( + 'Unable to stringify location ref, empty target', + ); }); }); diff --git a/packages/catalog-model/src/location/helpers.ts b/packages/catalog-model/src/location/helpers.ts index 1edd4e5ec5..d308c00449 100644 --- a/packages/catalog-model/src/location/helpers.ts +++ b/packages/catalog-model/src/location/helpers.ts @@ -14,8 +14,9 @@ * limitations under the License. */ +import { ANNOTATION_SOURCE_LOCATION } from '.'; import { Entity, stringifyEntityRef } from '../entity'; -import { LOCATION_ANNOTATION, SOURCE_LOCATION_ANNOTATION } from './annotation'; +import { ANNOTATION_LOCATION } from './annotation'; /** * Parses a string form location reference. @@ -26,36 +27,53 @@ import { LOCATION_ANNOTATION, SOURCE_LOCATION_ANNOTATION } from './annotation'; * @public * @param ref - A string-form location reference, e.g. `'url:https://host'` * @returns A location reference, e.g. `{ type: 'url', target: 'https://host' }` + * @deprecated use {@link parseLocationRef} instead */ -export function parseLocationReference(ref: string): { +export function parseLocationReference(ref: string) { + return parseLocationRef(ref); +} + +/** + * Parses a string form location reference. + * + * @remarks + * + * Note that the return type is not `LocationSpec`, because we do not want to + * conflate the string form with the additional properties of that type. + * + * @public + * @param ref - A string-form location ref, e.g. `'url:https://host'` + * @returns A location ref, e.g. `{ type: 'url', target: 'https://host' }` + */ +export function parseLocationRef(ref: string): { type: string; target: string; } { if (typeof ref !== 'string') { throw new TypeError( - `Unable to parse location reference '${ref}', unexpected argument ${typeof ref}`, + `Unable to parse location ref '${ref}', unexpected argument ${typeof ref}`, ); } const splitIndex = ref.indexOf(':'); if (splitIndex < 0) { throw new TypeError( - `Unable to parse location reference '${ref}', expected ':', e.g. 'url:https://host/path'`, + `Unable to parse location ref '${ref}', expected ':', e.g. 'url:https://host/path'`, ); } - const type = ref.substr(0, splitIndex).trim(); - const target = ref.substr(splitIndex + 1).trim(); + const type = ref.substring(0, splitIndex).trim(); + const target = ref.substring(splitIndex + 1).trim(); if (!type || !target) { throw new TypeError( - `Unable to parse location reference '${ref}', expected ':', e.g. 'url:https://host/path'`, + `Unable to parse location ref '${ref}', expected ':', e.g. 'url:https://host/path'`, ); } if (type === 'http' || type === 'https') { throw new TypeError( - `Invalid location reference '${ref}', please prefix it with 'url:', e.g. 'url:${ref}'`, + `Invalid location ref '${ref}', please prefix it with 'url:', e.g. 'url:${ref}'`, ); } @@ -73,17 +91,37 @@ export function parseLocationReference(ref: string): { * @public * @param ref - A location reference, e.g. `{ type: 'url', target: 'https://host' }` * @returns A string-form location reference, e.g. `'url:https://host'` + * @deprecated use {@link stringifyLocationRef} instead */ export function stringifyLocationReference(ref: { type: string; target: string; +}): string { + return stringifyLocationRef(ref); +} + +/** + * Turns a location ref into its string form. + * + * @remarks + * + * Note that the input type is not `LocationSpec`, because we do not want to + * conflate the string form with the additional properties of that type. + * + * @public + * @param ref - A location ref, e.g. `{ type: 'url', target: 'https://host' }` + * @returns A string-form location ref, e.g. `'url:https://host'` + */ +export function stringifyLocationRef(ref: { + type: string; + target: string; }): string { const { type, target } = ref; if (!type) { - throw new TypeError(`Unable to stringify location reference, empty type`); + throw new TypeError(`Unable to stringify location ref, empty type`); } else if (!target) { - throw new TypeError(`Unable to stringify location reference, empty target`); + throw new TypeError(`Unable to stringify location ref, empty target`); } return `${type}:${target}`; @@ -105,8 +143,8 @@ export function getEntitySourceLocation(entity: Entity): { target: string; } { const locationRef = - entity.metadata?.annotations?.[SOURCE_LOCATION_ANNOTATION] ?? - entity.metadata?.annotations?.[LOCATION_ANNOTATION]; + entity.metadata?.annotations?.[ANNOTATION_SOURCE_LOCATION] ?? + entity.metadata?.annotations?.[ANNOTATION_LOCATION]; if (!locationRef) { throw new Error( @@ -114,5 +152,5 @@ export function getEntitySourceLocation(entity: Entity): { ); } - return parseLocationReference(locationRef); + return parseLocationRef(locationRef); } diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts index ead4a6b564..8302ec202f 100644 --- a/packages/catalog-model/src/location/index.ts +++ b/packages/catalog-model/src/location/index.ts @@ -15,18 +15,18 @@ */ export { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, + ANNOTATION_SOURCE_LOCATION, LOCATION_ANNOTATION, ORIGIN_LOCATION_ANNOTATION, SOURCE_LOCATION_ANNOTATION, } from './annotation'; export { - parseLocationReference, - stringifyLocationReference, getEntitySourceLocation, + parseLocationRef, + parseLocationReference, + stringifyLocationRef, + stringifyLocationReference, } from './helpers'; export type { Location, LocationSpec } from './types'; -export { - analyzeLocationSchema, - locationSchema, - locationSpecSchema, -} from './validation'; diff --git a/packages/catalog-model/src/location/types.ts b/packages/catalog-model/src/location/types.ts index 725f873f2d..2ed8fd717b 100644 --- a/packages/catalog-model/src/location/types.ts +++ b/packages/catalog-model/src/location/types.ts @@ -35,6 +35,7 @@ export type LocationSpec = { * Entity location for a specific entity. * * @public + * @deprecated import from {@link @backstage/catalog-client#Location} instead. */ export type Location = { id: string; diff --git a/packages/catalog-model/src/location/validation.ts b/packages/catalog-model/src/location/validation.ts deleted file mode 100644 index 2fd692e649..0000000000 --- a/packages/catalog-model/src/location/validation.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as yup from 'yup'; -import { LocationSpec, Location } from './types'; - -/** - * Deprecated. - * - * @public - * @deprecated Use {@link JSONSchema} or validators instead. - */ -export const locationSpecSchema: yup.SchemaOf = yup - .object({ - type: yup.string().required(), - target: yup.string().required(), - presence: yup.mixed().oneOf(['required', 'optional']), - }) - .noUnknown() - .required(); - -/** - * Deprecated. - * - * @public - * @deprecated Use {@link JSONSchema} or validators instead. - */ -export const locationSchema: yup.SchemaOf = yup - .object({ - id: yup.string().required(), - type: yup.string().required(), - target: yup.string().required(), - presence: yup.mixed().oneOf(['required', 'optional']), - }) - .noUnknown() - .required(); - -/** - * Deprecated. - * - * @public - * @deprecated Use {@link JSONSchema} or validators instead. - */ -export const analyzeLocationSchema: yup.SchemaOf<{ location: LocationSpec }> = - yup - .object({ - location: locationSpecSchema, - }) - .noUnknown() - .required(); diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7d63801beb..37bea1e016 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/cli +## 0.13.2 + +### Patch Changes + +- bbbaa8ed61: The `plugin:diff` command no longer validates the existence of any of the files within `dev/` or `src/`. +- eaf67f0578: Introduced initial support for an experimental `backstage.role` field in package.json, as well as experimental and hidden `migrate` and `script` sub-commands. We do not recommend usage of any of these additions yet. +- aeb5c69abb: Introduces a new `--release` parameter to the `backstage-cli versions:bump` command. + The release can be either a specific version, for example `0.99.1`, or the latest `main` or `next` release. + The default behavior is to bump to the latest `main` release. +- d59b90852a: The experimental types build enabled by `--experimental-type-build` now runs in a separate worker thread. +- 50a19ff8dd: The file path printed by the default lint formatter is now relative to the repository root, rather than the individual package. +- 63181dee79: Tweaked frontend bundling configuration to avoid leaking declarations into global scope. +- fae2aee878: Removed the `import/no-duplicates` lint rule from the frontend and backend ESLint configurations. This rule is quite expensive to execute and only provides a purely cosmetic benefit, so we opted to remove it from the set of default rules. If you would like to keep this rule you can add it back in your local ESLint configuration: + + ```js + 'import/no-duplicates': 'warn' + ``` + +- b906f98119: Rather than calling `yarn pack`, the `build-workspace` and `backend-bundle` commands now move files directly whenever possible. This cuts out several `yarn` invocations and speeds the packing process up by several orders of magnitude. +- d0c71e2aa4: Switched the `lint` command to invoke ESLint directly through its Node.js API rather than spawning a new process. +- d59b90852a: Introduced an experimental and hidden `repo` sub-command, that contains commands that operate on an entire monorepo rather than individual packages. +- Updated dependencies + - @backstage/release-manifests@0.0.1 + +## 0.13.2-next.0 + +### Patch Changes + +- bbbaa8ed61: The `plugin:diff` command no longer validates the existence of any of the files within `dev/` or `src/`. +- eaf67f0578: Introduced initial support for an experimental `backstage.role` field in package.json, as well as experimental and hidden `migrate` and `script` sub-commands. We do not recommend usage of any of these additions yet. +- d59b90852a: The experimental types build enabled by `--experimental-type-build` now runs in a separate worker thread. +- 50a19ff8dd: The file path printed by the default lint formatter is now relative to the repository root, rather than the individual package. +- 63181dee79: Tweaked frontend bundling configuration to avoid leaking declarations into global scope. +- fae2aee878: Removed the `import/no-duplicates` lint rule from the frontend and backend ESLint configurations. This rule is quite expensive to execute and only provides a purely cosmetic benefit, so we opted to remove it from the set of default rules. If you would like to keep this rule you can add it back in your local ESLint configuration: + + ```js + 'import/no-duplicates': 'warn' + ``` + +- b906f98119: Rather than calling `yarn pack`, the `build-workspace` and `backend-bundle` commands now move files directly whenever possible. This cuts out several `yarn` invocations and speeds the packing process up by several orders of magnitude. +- d0c71e2aa4: Switched the `lint` command to invoke ESLint directly through its Node.js API rather than spawning a new process. +- d59b90852a: Introduced an experimental and hidden `repo` sub-command, that contains commands that operate on an entire monorepo rather than individual packages. + +## 0.13.1 + +### Patch Changes + +- 5bd0ce9e62: chore(deps): bump `inquirer` from 7.3.3 to 8.2.0 +- 80f510caee: Log warning if unable to parse yarn.lock + +## 0.13.1-next.1 + +### Patch Changes + +- 5bd0ce9e62: chore(deps): bump `inquirer` from 7.3.3 to 8.2.0 + +## 0.13.1-next.0 + +### Patch Changes + +- 80f510caee: Log warning if unable to parse yarn.lock + ## 0.13.0 ### Minor Changes diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index a97f26f8da..519b388b96 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -54,11 +54,10 @@ module.exports = { 'no-console': 0, // Permitted in console programs 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' 'import/newline-after-import': 'error', - 'import/no-duplicates': 'warn', 'import/no-extraneous-dependencies': [ 'error', { - devDependencies: false, + devDependencies: ['**/*.test.*', 'src/setupTests.*', 'dev/**'], optionalDependencies: true, peerDependencies: true, bundledDependencies: true, @@ -97,16 +96,6 @@ module.exports = { { files: ['*.test.*', 'src/setupTests.*', 'dev/**'], rules: { - // Tests are allowed to import dev dependencies - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], 'no-restricted-syntax': ['error', ...globalRestrictedSyntax], }, }, diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 781af13577..06f22933e8 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -49,11 +49,15 @@ module.exports = { '@typescript-eslint/no-redeclare': 'error', 'no-undef': 'off', 'import/newline-after-import': 'error', - 'import/no-duplicates': 'warn', 'import/no-extraneous-dependencies': [ 'error', { - devDependencies: false, + devDependencies: [ + '**/*.test.*', + '**/*.stories.*', + 'src/setupTests.*', + 'dev/**', + ], optionalDependencies: true, peerDependencies: true, bundledDependencies: true, @@ -101,20 +105,5 @@ module.exports = { 'no-undef': 'off', }, }, - { - files: ['*.test.*', '*.stories.*', 'src/setupTests.*', 'dev/**'], - rules: { - // Tests are allowed to import dev dependencies - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], - }, - }, ], }; diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 978c598584..91cdd3c41b 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -102,9 +102,10 @@ async function getProjectConfig(targetPath, displayName) { }, transform: { - '\\.(js|jsx|ts|tsx|mjs|cjs)$': require.resolve( - './jestSucraseTransform.js', - ), + '\\.(js|jsx|ts|tsx|mjs|cjs)$': [ + require.resolve('./jestSucraseTransform.js'), + { enableSourceMaps: Boolean(process.env.ENABLE_SOURCE_MAPS) }, + ], '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$': require.resolve('./jestFileTransform.js'), '\\.(yaml)$': require.resolve('jest-transform-yaml'), diff --git a/packages/cli/config/jestSucraseTransform.js b/packages/cli/config/jestSucraseTransform.js index c88909d21f..d0400f4f3b 100644 --- a/packages/cli/config/jestSucraseTransform.js +++ b/packages/cli/config/jestSucraseTransform.js @@ -21,50 +21,71 @@ const sucrasePluginPkg = require('@sucrase/jest-plugin/package.json'); const ESM_REGEX = /\b(?:import|export)\b/; -function process(source, filePath) { - let transforms; +function createTransformer(config) { + const process = (source, filePath) => { + let transforms; - if (filePath.endsWith('.esm.js')) { - transforms = ['imports']; - } else if (filePath.endsWith('.js')) { - // This is a very rough filter to avoid transforming things that we quickly - // can be sure are definitely not ESM modules. - if (ESM_REGEX.test(source)) { - transforms = ['imports', 'jsx']; // JSX within .js is currently allowed + if (filePath.endsWith('.esm.js')) { + transforms = ['imports']; + } else if (filePath.endsWith('.js')) { + // This is a very rough filter to avoid transforming things that we quickly + // can be sure are definitely not ESM modules. + if (ESM_REGEX.test(source)) { + transforms = ['imports', 'jsx']; // JSX within .js is currently allowed + } + } else if (filePath.endsWith('.jsx')) { + transforms = ['jsx', 'imports']; + } else if (filePath.endsWith('.ts')) { + transforms = ['typescript', 'imports']; + } else if (filePath.endsWith('.tsx')) { + transforms = ['typescript', 'jsx', 'imports']; } - } else if (filePath.endsWith('.jsx')) { - transforms = ['jsx', 'imports']; - } else if (filePath.endsWith('.ts')) { - transforms = ['typescript', 'imports']; - } else if (filePath.endsWith('.tsx')) { - transforms = ['typescript', 'jsx', 'imports']; - } - // Only apply the jest transform to the test files themselves - if (transforms && filePath.includes('.test.')) { - transforms.push('jest'); - } + // Only apply the jest transform to the test files themselves + if (transforms && filePath.includes('.test.')) { + transforms.push('jest'); + } - if (transforms) { - return transform(source, { - transforms, - filePath, - disableESTransforms: true, - }).code; - } + if (transforms) { + const { code, sourceMap: map } = transform(source, { + transforms, + filePath, + disableESTransforms: true, + sourceMapOptions: { + compiledFilename: filePath, + }, + }); + if (config.enableSourceMaps) { + const b64 = Buffer.from(JSON.stringify(map), 'utf8').toString('base64'); + const suffix = `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}`; + // Include both inline and object source maps, as inline source maps are + // needed for support of some editor integrations. + return { code: `${code}\n${suffix}`, map }; + } + // We only return the `map` result if source maps are enabled, as they + // have a negative impact on the coverage accuracy. + return code; + } - return source; + return source; + }; + + // TODO: contribute something like this to @sucrase/jest-plugin + const getCacheKey = sourceText => { + return createHash('md5') + .update(sourceText) + .update(Buffer.alloc(1)) + .update(sucrasePkg.version) + .update(Buffer.alloc(1)) + .update(sucrasePluginPkg.version) + .update(Buffer.alloc(1)) + .update(JSON.stringify(config)) + .update(Buffer.alloc(1)) + .update('1') // increment whenever the transform logic in this file changes + .digest('hex'); + }; + + return { process, getCacheKey }; } -// TODO: contribute something like this to @sucrase/jest-plugin -function getCacheKey(sourceText) { - return createHash('md5') - .update(sourceText) - .update(Buffer.alloc(1)) - .update(sucrasePkg.version) - .update(Buffer.alloc(1)) - .update(sucrasePluginPkg.version) - .digest('hex'); -} - -module.exports = { process, getCacheKey }; +module.exports = { createTransformer }; diff --git a/packages/cli/package.json b/packages/cli/package.json index 8979387896..589ac58646 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.13.0", + "version": "0.13.2", "private": false, "publishConfig": { "access": "public" @@ -32,6 +32,7 @@ "@backstage/config": "^0.1.13", "@backstage/config-loader": "^0.9.3", "@backstage/errors": "^0.2.0", + "@backstage/release-manifests": "^0.0.1", "@backstage/types": "^0.1.1", "@hot-loader/react-dom": "^16.13.0", "@manypkg/get-packages": "^1.1.3", @@ -78,7 +79,7 @@ "glob": "^7.1.7", "handlebars": "^4.7.3", "html-webpack-plugin": "^5.3.1", - "inquirer": "^7.0.4", + "inquirer": "^8.2.0", "jest": "^26.0.1", "jest-css-modules": "^2.1.0", "json-schema": "^0.4.0", @@ -86,6 +87,7 @@ "lodash": "^4.17.21", "minimatch": "3.0.4", "mini-css-extract-plugin": "^2.4.2", + "npm-packlist": "^3.0.0", "node-libs-browser": "^2.2.1", "ora": "^5.3.0", "postcss": "^8.1.0", @@ -112,16 +114,17 @@ "webpack-node-externals": "^3.0.0", "yaml": "^1.10.0", "yml-loader": "^2.1.0", - "yn": "^4.0.0" + "yn": "^4.0.0", + "zod": "^3.11.6" }, "devDependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@backstage/theme": "^0.2.14", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", @@ -131,6 +134,7 @@ "@types/minimatch": "^3.0.5", "@types/mock-fs": "^4.13.0", "@types/node": "^14.14.32", + "@types/npm-packlist": "^1.1.2", "@types/recursive-readdir": "^2.2.0", "@types/rollup-plugin-peer-deps-external": "^2.2.0", "@types/rollup-plugin-postcss": "^2.0.0", @@ -140,7 +144,8 @@ "del": "^6.0.0", "mock-fs": "^5.1.0", "nodemon": "^2.0.2", - "ts-node": "^10.0.0" + "ts-node": "^10.0.0", + "msw": "^0.35.0" }, "peerDependencies": { "@microsoft/api-extractor": "^7.19.2" diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts index 36d910fe5b..67b71632c7 100644 --- a/packages/cli/src/commands/app/build.ts +++ b/packages/cli/src/commands/app/build.ts @@ -17,7 +17,7 @@ import fs from 'fs-extra'; import { Command } from 'commander'; import { buildBundle } from '../../lib/bundler'; -import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; +import { getEnvironmentParallelism } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; import { paths } from '../../lib/paths'; @@ -25,7 +25,7 @@ export default async (cmd: Command) => { const { name } = await fs.readJson(paths.resolveTarget('package.json')); await buildBundle({ entry: 'src/index', - parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), + parallelism: getEnvironmentParallelism(), statsJsonEnabled: cmd.stats, ...(await loadCliConfig({ args: cmd.config, diff --git a/packages/cli/src/commands/backend/bundle.ts b/packages/cli/src/commands/backend/bundle.ts index 338d5cdaa0..ec5fa0a131 100644 --- a/packages/cli/src/commands/backend/bundle.ts +++ b/packages/cli/src/commands/backend/bundle.ts @@ -21,7 +21,7 @@ import tar, { CreateOptions } from 'tar'; import { Command } from 'commander'; import { createDistWorkspace } from '../../lib/packager'; import { paths } from '../../lib/paths'; -import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel'; +import { getEnvironmentParallelism } from '../../lib/parallel'; import { buildPackage, Output } from '../../lib/builder'; const BUNDLE_FILE = 'bundle.tar.gz'; @@ -40,7 +40,7 @@ export default async (cmd: Command) => { targetDir: tmpDir, buildDependencies: Boolean(cmd.buildDependencies), buildExcludes: [pkg.name], - parallel: parseParallel(process.env[PARALLEL_ENV_VAR]), + parallelism: getEnvironmentParallelism(), skeleton: SKELETON_FILE, }); diff --git a/packages/cli/src/commands/build/buildApp.ts b/packages/cli/src/commands/build/buildApp.ts new file mode 100644 index 0000000000..b0959b2d6a --- /dev/null +++ b/packages/cli/src/commands/build/buildApp.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { buildBundle } from '../../lib/bundler'; +import { getEnvironmentParallelism } from '../../lib/parallel'; +import { loadCliConfig } from '../../lib/config'; + +interface BuildAppOptions { + targetDir: string; + writeStats: boolean; + configPaths: string[]; +} + +export async function buildApp(options: BuildAppOptions) { + const { targetDir, writeStats, configPaths } = options; + const { name } = await fs.readJson(resolvePath(targetDir, 'package.json')); + await buildBundle({ + targetDir, + entry: 'src/index', + parallelism: getEnvironmentParallelism(), + statsJsonEnabled: writeStats, + ...(await loadCliConfig({ + args: configPaths, + fromPackage: name, + })), + }); +} diff --git a/packages/cli/src/commands/build/buildBackend.ts b/packages/cli/src/commands/build/buildBackend.ts new file mode 100644 index 0000000000..e32b7956db --- /dev/null +++ b/packages/cli/src/commands/build/buildBackend.ts @@ -0,0 +1,75 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import os from 'os'; +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import tar, { CreateOptions } from 'tar'; +import { createDistWorkspace } from '../../lib/packager'; +import { getEnvironmentParallelism } from '../../lib/parallel'; +import { buildPackage, Output } from '../../lib/builder'; + +const BUNDLE_FILE = 'bundle.tar.gz'; +const SKELETON_FILE = 'skeleton.tar.gz'; + +interface BuildBackendOptions { + targetDir: string; + skipBuildDependencies: boolean; +} + +export async function buildBackend(options: BuildBackendOptions) { + const { targetDir, skipBuildDependencies } = options; + const pkg = await fs.readJson(resolvePath(targetDir, 'package.json')); + + // We build the target package without generating type declarations. + await buildPackage({ outputs: new Set([Output.cjs]) }); + + const tmpDir = await fs.mkdtemp(resolvePath(os.tmpdir(), 'backstage-bundle')); + try { + await createDistWorkspace([pkg.name], { + targetDir: tmpDir, + buildDependencies: !skipBuildDependencies, + buildExcludes: [pkg.name], + parallelism: getEnvironmentParallelism(), + skeleton: SKELETON_FILE, + }); + + // We built the target backend package using the regular build process, but the result of + // that has now been packed into the dist workspace, so clean up the dist dir. + await fs.remove(targetDir); + await fs.mkdir(targetDir); + + // Move out skeleton.tar.gz before we create the main bundle, no point having that included up twice. + await fs.move( + resolvePath(tmpDir, SKELETON_FILE), + resolvePath(targetDir, SKELETON_FILE), + ); + + // Create main bundle.tar.gz, with some tweaks to make it more likely hit Docker build cache. + await tar.create( + { + file: resolvePath(targetDir, BUNDLE_FILE), + cwd: tmpDir, + portable: true, + noMtime: true, + gzip: true, + } as CreateOptions & { noMtime: boolean }, + [''], + ); + } finally { + await fs.remove(tmpDir); + } +} diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts new file mode 100644 index 0000000000..1189883b12 --- /dev/null +++ b/packages/cli/src/commands/build/command.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Command } from 'commander'; +import { buildPackage, Output } from '../../lib/builder'; +import { findRoleFromCommand, getRoleInfo } from '../../lib/role'; +import { paths } from '../../lib/paths'; +import { buildApp } from './buildApp'; +import { buildBackend } from './buildBackend'; + +export async function command(cmd: Command): Promise { + const role = await findRoleFromCommand(cmd); + + if (role === 'app') { + return buildApp({ + targetDir: paths.resolveTarget('dist'), + configPaths: cmd.config as string[], + writeStats: Boolean(cmd.stats), + }); + } + if (role === 'backend') { + return buildBackend({ + targetDir: paths.resolveTarget('dist'), + skipBuildDependencies: Boolean(cmd.skipBuildDependencies), + }); + } + + const roleInfo = getRoleInfo(role); + + const outputs = new Set(); + + if (roleInfo.output.includes('cjs')) { + outputs.add(Output.cjs); + } + if (roleInfo.output.includes('esm')) { + outputs.add(Output.esm); + } + if (roleInfo.output.includes('types')) { + outputs.add(Output.types); + } + + return buildPackage({ + outputs, + minify: Boolean(cmd.minify), + useApiExtractor: Boolean(cmd.experimentalTypeBuild), + }); +} diff --git a/packages/cli/src/commands/build/index.ts b/packages/cli/src/commands/build/index.ts new file mode 100644 index 0000000000..680fe9e11d --- /dev/null +++ b/packages/cli/src/commands/build/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { command } from './command'; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 362771f8f2..770d216ba2 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -18,14 +18,131 @@ import { assertError } from '@backstage/errors'; import { CommanderStatic } from 'commander'; import { exitWithError } from '../lib/errors'; -export function registerCommands(program: CommanderStatic) { - const configOption = [ - '--config ', - 'Config files to load instead of app-config.yaml', - (opt: string, opts: string[]) => [...opts, opt], - Array(), - ] as const; +const configOption = [ + '--config ', + 'Config files to load instead of app-config.yaml', + (opt: string, opts: string[]) => [...opts, opt], + Array(), +] as const; +export function registerRepoCommand(program: CommanderStatic) { + const command = program + .command('repo [command]', { hidden: true }) + .description( + 'Command that run across an entire Backstage project [EXPERIMENTAL]', + ); + + command + .command('build') + .description( + 'Build packages in the project, excluding bundled app and backend packages.', + ) + .option( + '--all', + 'Build all packages, including bundled app and backend packages.', + ) + .action(lazy(() => import('./repo/build').then(m => m.command))); +} + +export function registerScriptCommand(program: CommanderStatic) { + const command = program + .command('script [command]', { hidden: true }) + .description('Lifecycle scripts for Backstage packages [EXPERIMENTAL]'); + + command + .command('start') + .description('Start a package for local development') + .option(...configOption) + .option('--role ', 'Run the command with an explicit package role') + .option('--check', 'Enable type checking and linting if available') + .option('--inspect', 'Enable debugger in Node.js environments') + .option( + '--inspect-brk', + 'Enable debugger in Node.js environments, breaking before code starts', + ) + .action(lazy(() => import('./start').then(m => m.command))); + + command + .command('build') + .description('Build a package for production deployment or publishing') + .option( + '--minify', + 'Minify the generated code. Does not apply to app or backend packages.', + ) + .option( + '--experimental-type-build', + 'Enable experimental type build. Does not apply to app or backend packages.', + ) + .option( + '--skip-build-dependencies', + 'Skip the automatic building of local dependencies. Applies to backend packages only.', + ) + .option( + '--stats', + 'If bundle stats are available, write them to the output directory. Applies to app packages only.', + ) + .option( + '--config ', + 'Config files to load instead of app-config.yaml. Applies to app packages only.', + (opt: string, opts: string[]) => [...opts, opt], + Array(), + ) + .action(lazy(() => import('./build').then(m => m.command))); + + program + .command('lint') + .option( + '--format ', + 'Lint report output format', + 'eslint-formatter-friendly', + ) + .option('--fix', 'Attempt to automatically fix violations') + .description('Lint a package') + .action(lazy(() => import('./lint').then(m => m.default))); + + program + .command('test') + .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args + .helpOption(', --backstage-cli-help') // Let Jest handle help + .description('Run tests, forwarding args to Jest, defaulting to watch mode') + .action(lazy(() => import('./testCommand').then(m => m.default))); + + command + .command('clean') + .description('Delete cache directories') + .action(lazy(() => import('./clean/clean').then(m => m.default))); + + command + .command('prepack') + .description('Prepares a package for packaging before publishing') + .action(lazy(() => import('./pack').then(m => m.pre))); + + command + .command('postpack') + .description('Restores the changes made by the prepack command') + .action(lazy(() => import('./pack').then(m => m.post))); +} + +export function registerMigrateCommand(program: CommanderStatic) { + const command = program + .command('migrate [command]', { hidden: true }) + .description('Migration utilities [EXPERIMENTAL]'); + + command + .command('package-role') + .description(`Add package role field to packages that don't have it`) + .action(lazy(() => import('./migrate/packageRole').then(m => m.default))); + + command + .command('package-scripts') + .description('Set package scripts according to each package role') + .action( + lazy(() => import('./migrate/packageScripts').then(m => m.command)), + ); +} + +export function registerCommands(program: CommanderStatic) { + // TODO(Rugvip): Deprecate in favor of script variant program .command('app:build') .description('Build an app for a production release') @@ -33,6 +150,7 @@ export function registerCommands(program: CommanderStatic) { .option(...configOption) .action(lazy(() => import('./app/build').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('app:serve') .description('Serve an app for local development') @@ -40,6 +158,7 @@ export function registerCommands(program: CommanderStatic) { .option(...configOption) .action(lazy(() => import('./app/serve').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('backend:build') .description('Build a backend plugin') @@ -47,6 +166,7 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./backend/build').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('backend:bundle') .description('Bundle the backend into a deployment archive') @@ -56,6 +176,7 @@ export function registerCommands(program: CommanderStatic) { ) .action(lazy(() => import('./backend/bundle').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('backend:dev') .description('Start local development server with HMR for the backend') @@ -104,6 +225,7 @@ export function registerCommands(program: CommanderStatic) { lazy(() => import('./create-plugin/createPlugin').then(m => m.default)), ); + // TODO(Rugvip): Deprecate in favor of script variant program .command('plugin:build') .description('Build a plugin') @@ -111,6 +233,7 @@ export function registerCommands(program: CommanderStatic) { .option('--experimental-type-build', 'Enable experimental type build') .action(lazy(() => import('./plugin/build').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('plugin:serve') .description('Serves the dev/ folder of a plugin') @@ -125,14 +248,16 @@ export function registerCommands(program: CommanderStatic) { .description('Diff an existing plugin with the creation template') .action(lazy(() => import('./plugin/diff').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('build') .description('Build a package for publishing') .option('--outputs ', 'List of formats to output [types,cjs,esm]') .option('--minify', 'Minify the generated code') .option('--experimental-type-build', 'Enable experimental type build') - .action(lazy(() => import('./build').then(m => m.default))); + .action(lazy(() => import('./oldBuild').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('lint') .option( @@ -144,6 +269,7 @@ export function registerCommands(program: CommanderStatic) { .description('Lint a package') .action(lazy(() => import('./lint').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('test') .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args @@ -205,12 +331,21 @@ export function registerCommands(program: CommanderStatic) { .description('Print configuration schema') .action(lazy(() => import('./config/schema').then(m => m.default))); + registerRepoCommand(program); + registerScriptCommand(program); + registerMigrateCommand(program); + program .command('versions:bump') .option( '--pattern ', 'Override glob for matching packages to upgrade', ) + .option( + '--release ', + 'Bump to a specific Backstage release line or version', + 'main', + ) .description('Bump Backstage packages to the latest versions') .action(lazy(() => import('./versions/bump').then(m => m.default))); @@ -220,16 +355,19 @@ export function registerCommands(program: CommanderStatic) { .description('Check Backstage package versioning') .action(lazy(() => import('./versions/lint').then(m => m.default))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('prepack') .description('Prepares a package for packaging before publishing') .action(lazy(() => import('./pack').then(m => m.pre))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('postpack') .description('Restores the changes made by the prepack command') .action(lazy(() => import('./pack').then(m => m.post))); + // TODO(Rugvip): Deprecate in favor of script variant program .command('clean') .description('Delete cache directories') diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 5c3e0a88e3..823d24fbe8 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -15,19 +15,34 @@ */ import { Command } from 'commander'; -import { run } from '../lib/run'; import { paths } from '../lib/paths'; +import { ESLint } from 'eslint'; export default async (cmd: Command, cmdArgs: string[]) => { - const args = [ - '--ext=js,jsx,ts,tsx,mjs,cjs', - '--max-warnings=0', - `--format=${cmd.format}`, - ...(cmdArgs ?? [paths.targetDir]), - ]; + const eslint = new ESLint({ + cwd: paths.targetDir, + fix: cmd.fix, + extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'], + }); + + const results = await eslint.lintFiles(cmdArgs ?? ['.']); + if (cmd.fix) { - args.push('--fix'); + await ESLint.outputFixes(results); } - await run('eslint', args); + const formatter = await eslint.loadFormatter(cmd.format); + + // This formatter uses the cwd to format file paths, so let's have that happen from the root instead + if (cmd.format === 'eslint-formatter-friendly') { + process.chdir(paths.targetRoot); + } + const resultText = formatter.format(results); + + // If there is any feedback at all, we treat it as a lint failure. This should be + // consistent with our old behavior of passing `--max-warnings=0` when invoking eslint. + if (resultText) { + console.log(resultText); + process.exit(1); + } }; diff --git a/packages/cli/src/commands/migrate/packageRole.ts b/packages/cli/src/commands/migrate/packageRole.ts new file mode 100644 index 0000000000..e8bfb8e7c8 --- /dev/null +++ b/packages/cli/src/commands/migrate/packageRole.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { getPackages } from '@manypkg/get-packages'; +import { paths } from '../../lib/paths'; +import { getRoleFromPackage, detectRoleFromPackage } from '../../lib/role'; + +export default async () => { + const { packages } = await getPackages(paths.targetDir); + + await Promise.all( + packages.map(async ({ dir, packageJson: pkg }) => { + const { name } = pkg; + const existingRole = getRoleFromPackage(pkg); + if (existingRole) { + return; + } + + const detectedRole = detectRoleFromPackage(pkg); + if (!detectedRole) { + console.error(`No role detected for package ${name}`); + return; + } + + console.log(`Detected package role of ${name} as ${detectedRole}`); + + let newPkg = pkg as any; + + const pkgKeys = Object.keys(pkg); + if (pkgKeys.includes('backstage')) { + newPkg.backstage = { + ...newPkg.backstage, + role: detectedRole, + }; + } else { + // We insert the backstage field after one of these fields, otherwise at the end + const index = + Math.max( + pkgKeys.indexOf('version'), + pkgKeys.indexOf('private'), + pkgKeys.indexOf('publishConfig'), + ) + 1 || pkgKeys.length; + + const pkgEntries = Object.entries(pkg); + pkgEntries.splice(index, 0, ['backstage', { role: detectedRole }]); + newPkg = Object.fromEntries(pkgEntries); + } + + await fs.writeJson(resolvePath(dir, 'package.json'), newPkg, { + spaces: 2, + }); + }), + ); +}; diff --git a/packages/cli/src/commands/migrate/packageScripts.ts b/packages/cli/src/commands/migrate/packageScripts.ts new file mode 100644 index 0000000000..a3fdd20018 --- /dev/null +++ b/packages/cli/src/commands/migrate/packageScripts.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; +import { PackageGraph } from '../../lib/monorepo'; +import { getRoleFromPackage, getRoleInfo, PackageRole } from '../../lib/role'; + +const noStartRoles: PackageRole[] = ['cli', 'common-library']; + +export async function command() { + const packages = await PackageGraph.listTargetPackages(); + + await Promise.all( + packages.map(async ({ dir, packageJson }) => { + const role = getRoleFromPackage(packageJson); + if (!role) { + return; + } + + const roleInfo = getRoleInfo(role); + const hasStart = !noStartRoles.includes(role); + const isBundled = roleInfo.output.includes('bundle'); + + const expectedScripts = { + ...(hasStart && { start: 'backstage-cli script start' }), + build: 'backstage-cli script build', + lint: 'backstage-cli script lint', + test: 'backstage-cli script test', + clean: 'backstage-cli script clean', + ...(!isBundled && { + postpack: 'backstage-cli script postpack', + prepack: 'backstage-cli script prepack', + }), + }; + + let changed = false; + const currentScripts: Record = + (packageJson.scripts = packageJson.scripts || {}); + + for (const [name, value] of Object.entries(expectedScripts)) { + if (currentScripts[name] !== value) { + changed = true; + currentScripts[name] = value; + } + } + + if (changed) { + console.log(`Updating scripts for ${packageJson.name}`); + await fs.writeJson(resolvePath(dir, 'package.json'), packageJson, { + spaces: 2, + }); + } + }), + ); +} diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/oldBuild.ts similarity index 100% rename from packages/cli/src/commands/build.ts rename to packages/cli/src/commands/oldBuild.ts diff --git a/packages/cli/src/commands/plugin/diff.ts b/packages/cli/src/commands/plugin/diff.ts index 6a0127794f..153bd16597 100644 --- a/packages/cli/src/commands/plugin/diff.ts +++ b/packages/cli/src/commands/plugin/diff.ts @@ -39,18 +39,13 @@ const fileHandlers = [ patterns: ['package.json'], handler: handlers.packageJson, }, - { - // Not all plugins have routes - patterns: ['src/routes.ts'], - handler: handlers.skip, - }, { // make sure files in 1st level of src/ and dev/ exist - patterns: ['.eslintrc.js', /^(src|dev)\/[^/]+$/], + patterns: ['.eslintrc.js'], handler: handlers.exists, }, { - patterns: ['README.md', 'tsconfig.json', /^src\//], + patterns: ['README.md', 'tsconfig.json', /^src\//, /^dev\//], handler: handlers.skip, }, ]; diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts new file mode 100644 index 0000000000..9c32386304 --- /dev/null +++ b/packages/cli/src/commands/repo/build.ts @@ -0,0 +1,167 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import chalk from 'chalk'; +import { Command } from 'commander'; +import { relative as relativePath } from 'path'; +import { buildPackages, getOutputsForRole } from '../../lib/builder'; +import { PackageGraph } from '../../lib/monorepo'; +import { ExtendedPackage } from '../../lib/monorepo/PackageGraph'; +import { runParallelWorkers } from '../../lib/parallel'; +import { paths } from '../../lib/paths'; +import { detectRoleFromPackage } from '../../lib/role'; +import { buildApp } from '../build/buildApp'; +import { buildBackend } from '../build/buildBackend'; + +function createScriptOptionsParser(anyCmd: Command, commandPath: string[]) { + // Regardless of what command instance is passed in we want to find + // the root command and resolve the path from there + let rootCmd = anyCmd; + while (rootCmd.parent) { + rootCmd = rootCmd.parent; + } + + // Now find the command that was requested + let targetCmd = rootCmd as Command | undefined; + for (const name of commandPath) { + targetCmd = targetCmd?.commands.find(c => c.name() === name) as + | Command + | undefined; + } + + if (!targetCmd) { + throw new Error(`Could not find script command '${commandPath.join(' ')}'`); + } + const cmd = targetCmd; + + const expectedScript = `backstage-cli ${commandPath.join(' ')}`; + + return (scriptStr?: string) => { + if (!scriptStr || !scriptStr.startsWith(expectedScript)) { + return undefined; + } + + const argsStr = scriptStr.slice(expectedScript.length).trim(); + + // Can't clone or copy or even use commands as prototype, so we mutate + // the necessary members instead, and then reset them once we're done + const currentOpts = cmd._optionValues; + const currentStore = cmd._storeOptionsAsProperties; + + const result: Record = {}; + cmd._storeOptionsAsProperties = false; + cmd._optionValues = result; + + // Triggers the writing of options to the result object + cmd.parseOptions(argsStr.split(' ')); + + cmd._storeOptionsAsProperties = currentOpts; + cmd._optionValues = currentStore; + + return result; + }; +} + +export async function command(cmd: Command): Promise { + const packages = await PackageGraph.listTargetPackages(); + const apps = new Array(); + const backends = new Array(); + + const parseBuildScript = createScriptOptionsParser(cmd, ['script', 'build']); + + const options = packages.flatMap(pkg => { + const role = + pkg.packageJson.backstage?.role ?? detectRoleFromPackage(pkg.packageJson); + if (!role) { + console.warn(`Ignored ${pkg.packageJson.name} because it has no role`); + return []; + } + + if (role === 'app') { + apps.push(pkg); + return []; + } else if (role === 'backend') { + backends.push(pkg); + return []; + } + + const outputs = getOutputsForRole(role); + if (outputs.size === 0) { + console.warn(`Ignored ${pkg.packageJson.name} because it has no output`); + return []; + } + + const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build); + if (!buildOptions) { + console.warn( + `Ignored ${pkg.packageJson.name} because it does not have a matching build script`, + ); + return []; + } + + return { + targetDir: pkg.dir, + outputs, + logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, + minify: buildOptions.minify, + useApiExtractor: buildOptions.experimentalTypeBuild, + }; + }); + + console.log('Building packages'); + await buildPackages(options); + + if (cmd.all) { + console.log('Building apps'); + await runParallelWorkers({ + items: apps, + parallelismFactor: 1 / 2, + worker: async pkg => { + const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build); + if (!buildOptions) { + console.warn( + `Ignored ${pkg.packageJson.name} because it does not have a matching build script`, + ); + return; + } + await buildApp({ + targetDir: pkg.dir, + configPaths: (buildOptions.config as string[]) ?? [], + writeStats: Boolean(buildOptions.stats), + }); + }, + }); + + console.log('Building backends'); + await runParallelWorkers({ + items: backends, + parallelismFactor: 1 / 2, + worker: async pkg => { + const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build); + if (!buildOptions) { + console.warn( + `Ignored ${pkg.packageJson.name} because it does not have a matching build script`, + ); + return; + } + await buildBackend({ + targetDir: pkg.dir, + skipBuildDependencies: true, + }); + }, + }); + } +} diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts new file mode 100644 index 0000000000..6615d5fd46 --- /dev/null +++ b/packages/cli/src/commands/start/command.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Command } from 'commander'; +import { startBackend } from './startBackend'; +import { startFrontend } from './startFrontend'; +import { findRoleFromCommand } from '../../lib/role'; + +export async function command(cmd: Command): Promise { + const role = await findRoleFromCommand(cmd); + + const options = { + configPaths: cmd.config as string[], + checksEnabled: Boolean(cmd.check), + inspectEnabled: Boolean(cmd.inspect), + inspectBrkEnabled: Boolean(cmd.inspectBrk), + }; + + switch (role) { + case 'backend': + case 'plugin-backend': + case 'plugin-backend-module': + case 'node-library': + return startBackend(options); + case 'app': + return startFrontend({ + ...options, + entry: 'src/index', + verifyVersions: true, + }); + case 'web-library': + case 'plugin-frontend': + case 'plugin-frontend-module': + return startFrontend({ entry: 'dev/index', ...options }); + default: + throw new Error( + `Start command is not supported for package role '${role}'`, + ); + } +} diff --git a/packages/cli/src/commands/start/index.ts b/packages/cli/src/commands/start/index.ts new file mode 100644 index 0000000000..680fe9e11d --- /dev/null +++ b/packages/cli/src/commands/start/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { command } from './command'; diff --git a/packages/cli/src/commands/start/startBackend.ts b/packages/cli/src/commands/start/startBackend.ts new file mode 100644 index 0000000000..61ada7a291 --- /dev/null +++ b/packages/cli/src/commands/start/startBackend.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import { paths } from '../../lib/paths'; +import { serveBackend } from '../../lib/bundler'; + +interface StartBackendOptions { + checksEnabled: boolean; + inspectEnabled: boolean; + inspectBrkEnabled: boolean; +} + +export async function startBackend(options: StartBackendOptions) { + // Cleaning dist/ before we start the dev process helps work around an issue + // where we end up with the entrypoint executing multiple times, causing + // a port bind conflict among other things. + await fs.remove(paths.resolveTarget('dist')); + + const waitForExit = await serveBackend({ + entry: 'src/index', + checksEnabled: options.checksEnabled, + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); + + await waitForExit(); +} diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts new file mode 100644 index 0000000000..e5500c310b --- /dev/null +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import chalk from 'chalk'; +import uniq from 'lodash/uniq'; +import { serveBundle } from '../../lib/bundler'; +import { loadCliConfig } from '../../lib/config'; +import { paths } from '../../lib/paths'; +import { Lockfile } from '../../lib/versioning'; +import { includedFilter } from '../versions/lint'; + +interface StartAppOptions { + verifyVersions?: boolean; + entry: string; + + checksEnabled: boolean; + configPaths: string[]; +} + +export async function startFrontend(options: StartAppOptions) { + if (options.verifyVersions) { + const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + const result = lockfile.analyze({ + filter: includedFilter, + }); + const problemPackages = [...result.newVersions, ...result.newRanges].map( + ({ name }) => name, + ); + + if (problemPackages.length > 1) { + console.log( + chalk.yellow( + `⚠️ Some of the following packages may be outdated or have duplicate installations: + + ${uniq(problemPackages).join(', ')} + `, + ), + ); + console.log( + chalk.yellow( + `⚠️ This can be resolved using the following command: + + yarn backstage-cli versions:check --fix + `, + ), + ); + } + } + + const { name } = await fs.readJson(paths.resolveTarget('package.json')); + const waitForExit = await serveBundle({ + entry: options.entry, + checksEnabled: options.checksEnabled, + ...(await loadCliConfig({ + args: options.configPaths, + fromPackage: name, + withFilteredKeys: true, + })), + }); + + await waitForExit(); +} diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 2dadd06d1d..e7b3c858aa 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -20,11 +20,18 @@ import { Command } from 'commander'; import { resolve as resolvePath } from 'path'; import { paths } from '../../lib/paths'; import * as runObj from '../../lib/run'; -import bump, { bumpBackstageJsonVersion } from './bump'; -import { withLogCollector } from '@backstage/test-utils'; +import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; +import { + setupRequestMockHandlers, + withLogCollector, +} from '@backstage/test-utils'; +import { YarnInfoInspectData } from '../../lib/versioning/packages'; +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; // Remove log coloring to simplify log matching jest.mock('chalk', () => ({ + red: (str: string) => str, blue: (str: string) => str, cyan: (str: string) => str, green: (str: string) => str, @@ -32,12 +39,25 @@ jest.mock('chalk', () => ({ yellow: (str: string) => str, })); +jest.mock('ora', () => ({ + __esModule: true, + default({ prefixText }: any) { + console.log(prefixText); + return { + start: () => ({ + succeed: () => {}, + }), + }; + }, +})); + const REGISTRY_VERSIONS: { [name: string]: string } = { '@backstage/core': '1.0.6', '@backstage/core-api': '1.0.7', '@backstage/theme': '2.0.0', '@backstage-extra/custom': '1.1.0', '@backstage-extra/custom-two': '2.0.0', + '@backstage/create-app': '1.0.0', }; const HEADER = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. @@ -82,6 +102,8 @@ describe('bump', () => { mockFs.restore(); jest.resetAllMocks(); }); + const worker = setupServer(); + setupRequestMockHandlers(worker); it('should bump backstage dependencies', async () => { mockFs({ @@ -121,14 +143,25 @@ describe('bump', () => { }), ); jest.spyOn(runObj, 'run').mockResolvedValue(undefined); - + worker.use( + rest.get( + 'https://versions.backstage.io/v1/tags/main/manifest.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + packages: [], + }), + ), + ), + ); const { log: logs } = await withLogCollector(['log'], async () => { - await bump({ pattern: null } as unknown as Command); + await bump({ pattern: null, release: 'main' } as unknown as Command); }); expect(logs.filter(Boolean)).toEqual([ 'Using default pattern glob @backstage/*', - 'Checking for updates of @backstage/theme', 'Checking for updates of @backstage/core', + 'Checking for updates of @backstage/theme', 'Checking for updates of @backstage/core-api', 'Some packages are outdated, updating', 'unlocking @backstage/core@^1.0.3 ~> 1.0.6', @@ -142,7 +175,7 @@ describe('bump', () => { 'Version bump complete!', ]); - expect(runObj.runPlain).toHaveBeenCalledTimes(4); + expect(runObj.runPlain).toHaveBeenCalledTimes(3); expect(runObj.runPlain).toHaveBeenCalledWith( 'yarn', 'info', @@ -157,7 +190,11 @@ describe('bump', () => { ); expect(runObj.run).toHaveBeenCalledTimes(1); - expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']); + expect(runObj.run).toHaveBeenCalledWith( + 'yarn', + ['install'], + expect.any(Object), + ); const lockfileContents = await fs.readFile('/yarn.lock', 'utf8'); expect(lockfileContents).toBe(lockfileMockResult); @@ -179,6 +216,336 @@ describe('bump', () => { }); }); + it('should prefer dependency versions from release manifest', async () => { + mockFs({ + '/yarn.lock': lockfileMock, + '/package.json': JSON.stringify({ + workspaces: { + packages: ['packages/*'], + }, + }), + '/packages/a/package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', + }, + }), + '/packages/b/package.json': JSON.stringify({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', + '@backstage/theme': '^1.0.0', + }, + }), + }); + + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...path) => resolvePath('/', ...path)); + jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) => + JSON.stringify({ + type: 'inspect', + data: { + name: name, + 'dist-tags': { + latest: REGISTRY_VERSIONS[name], + }, + }, + }), + ); + jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + worker.use( + rest.get( + 'https://versions.backstage.io/v1/tags/main/manifest.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + releaseVersion: '0.0.1', + packages: [ + { + name: '@backstage/theme', + version: '5.0.0', + }, + { + name: '@backstage/create-app', + version: '3.0.0', + }, + ], + }), + ), + ), + ); + const { log: logs } = await withLogCollector(['log'], async () => { + await bump({ pattern: null, release: 'main' } as unknown as Command); + }); + expect(logs.filter(Boolean)).toEqual([ + 'Using default pattern glob @backstage/*', + 'Checking for updates of @backstage/core', + 'Checking for updates of @backstage/theme', + 'Checking for updates of @backstage/theme', + 'Checking for updates of @backstage/core-api', + 'Some packages are outdated, updating', + 'unlocking @backstage/core@^1.0.3 ~> 1.0.6', + 'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7', + 'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7', + 'bumping @backstage/theme in b to ^5.0.0', + 'Your project is now at version 0.0.1, which has been written to backstage.json', + 'Running yarn install to install new versions', + '⚠️ The following packages may have breaking changes:', + ' @backstage/theme : 1.0.0 ~> 5.0.0', + ' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md', + 'Version bump complete!', + ]); + + expect(runObj.runPlain).toHaveBeenCalledTimes(2); + expect(runObj.runPlain).toHaveBeenCalledWith( + 'yarn', + 'info', + '--json', + '@backstage/core', + ); + expect(runObj.runPlain).not.toHaveBeenCalledWith( + 'yarn', + 'info', + '--json', + '@backstage/theme', + ); + + expect(runObj.run).toHaveBeenCalledTimes(1); + expect(runObj.run).toHaveBeenCalledWith( + 'yarn', + ['install'], + expect.any(Object), + ); + + const lockfileContents = await fs.readFile('/yarn.lock', 'utf8'); + expect(lockfileContents).toBe(lockfileMockResult); + + const packageA = await fs.readJson('/packages/a/package.json'); + expect(packageA).toEqual({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', // not bumped since new version is within range + }, + }); + const packageB = await fs.readJson('/packages/b/package.json'); + expect(packageB).toEqual({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', // not bumped + '@backstage/theme': '^5.0.0', // bumped since newer + }, + }); + }); + + it('should only bump packages in the manifest when a specific release is specified', async () => { + mockFs({ + '/yarn.lock': lockfileMock, + '/package.json': JSON.stringify({ + workspaces: { + packages: ['packages/*'], + }, + }), + '/packages/a/package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', + }, + }), + '/packages/b/package.json': JSON.stringify({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', + '@backstage/theme': '^1.0.0', + }, + }), + }); + jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) => + JSON.stringify({ + type: 'inspect', + data: { + name: name, + 'dist-tags': { + latest: REGISTRY_VERSIONS[name], + }, + }, + }), + ); + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...path) => resolvePath('/', ...path)); + + jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + worker.use( + rest.get( + 'https://versions.backstage.io/v1/releases/1.0.0/manifest.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + releaseVersion: '2.0.0', + packages: [ + { name: '@backstage/core', version: '5.0.0' }, + { name: '@backstage/core-api', version: '5.0.0' }, + ], + }), + ), + ), + ); + const { log: logs } = await withLogCollector(['log'], async () => { + await expect( + bump({ pattern: null, release: '1.0.0' } as unknown as Command), + ).rejects.toThrow('Duplicate versions present after package bump'); + }); + expect(logs.filter(Boolean)).toEqual([ + 'Using default pattern glob @backstage/*', + 'Checking for updates of @backstage/core', + 'Checking for updates of @backstage/theme', + 'Package info not found, ignoring package @backstage/theme', + 'Checking for updates of @backstage/core', + 'Checking for updates of @backstage/theme', + 'Checking for updates of @backstage/core-api', + 'Package info not found, ignoring package @backstage/theme', + 'Some packages are outdated, updating', + 'bumping @backstage/core in a to ^5.0.0', + 'bumping @backstage/core in b to ^5.0.0', + 'Your project is now at version 2.0.0, which has been written to backstage.json', + 'Running yarn install to install new versions', + '⚠️ The following packages may have breaking changes:', + ' @backstage/core : 1.0.3 ~> 5.0.0', + ' https://github.com/backstage/backstage/blob/master/packages/core/CHANGELOG.md', + 'Version bump complete!', + ]); + + expect(runObj.run).toHaveBeenCalledTimes(1); + expect(runObj.run).toHaveBeenCalledWith( + 'yarn', + ['install'], + expect.any(Object), + ); + + const packageA = await fs.readJson('/packages/a/package.json'); + expect(packageA).toEqual({ + name: 'a', + dependencies: { + '@backstage/core': '^5.0.0', + }, + }); + const packageB = await fs.readJson('/packages/b/package.json'); + expect(packageB).toEqual({ + name: 'b', + dependencies: { + '@backstage/core': '^5.0.0', + '@backstage/theme': '^1.0.0', + }, + }); + expect(await fs.readJson('/backstage.json')).toEqual({ version: '2.0.0' }); + }); + + it('should prefer versions from the highest manifest version when main is not specified', async () => { + mockFs({ + '/yarn.lock': lockfileMock, + '/package.json': JSON.stringify({ + workspaces: { + packages: ['packages/*'], + }, + }), + '/packages/a/package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': '^1.0.5', + }, + }), + '/packages/b/package.json': JSON.stringify({ + name: 'b', + dependencies: { + '@backstage/core': '^1.0.3', + '@backstage/theme': '^1.0.0', + }, + }), + }); + + jest + .spyOn(paths, 'resolveTargetRoot') + .mockImplementation((...path) => resolvePath('/', ...path)); + jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) => + JSON.stringify({ + type: 'inspect', + data: { + name: name, + 'dist-tags': { + latest: REGISTRY_VERSIONS[name], + }, + }, + }), + ); + jest.spyOn(runObj, 'run').mockResolvedValue(undefined); + worker.use( + rest.get( + 'https://versions.backstage.io/v1/tags/main/manifest.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + releaseVersion: '1.0.0', + packages: [ + { + name: '@backstage/theme', + version: '5.0.0', + }, + { + name: '@backstage/create-app', + version: '3.0.0', + }, + ], + }), + ), + ), + rest.get( + 'https://versions.backstage.io/v1/tags/next/manifest.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + releaseVersion: '1.0.0-next.1', + packages: [ + { + name: '@backstage/theme', + version: '4.0.0', + }, + { + name: '@backstage/create-app', + version: '2.0.0', + }, + ], + }), + ), + ), + ); + const { log: logs } = await withLogCollector(['log'], async () => { + await bump({ pattern: null, release: 'next' } as unknown as Command); + }); + expect(logs.filter(Boolean)).toEqual([ + 'Using default pattern glob @backstage/*', + 'Checking for updates of @backstage/core', + 'Checking for updates of @backstage/theme', + 'Checking for updates of @backstage/theme', + 'Checking for updates of @backstage/core-api', + 'Some packages are outdated, updating', + 'unlocking @backstage/core@^1.0.3 ~> 1.0.6', + 'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7', + 'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7', + 'bumping @backstage/theme in b to ^5.0.0', + 'Your project is now at version 1.0.0, which has been written to backstage.json', + 'Running yarn install to install new versions', + '⚠️ The following packages may have breaking changes:', + ' @backstage/theme : 1.0.0 ~> 5.0.0', + ' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md', + 'Version bump complete!', + ]); + }); + it('should bump backstage dependencies and dependencies matching pattern glob', async () => { const customLockfileMock = `${lockfileMock} "@backstage-extra/custom@^1.1.0": @@ -246,25 +613,40 @@ describe('bump', () => { }), ); jest.spyOn(runObj, 'run').mockResolvedValue(undefined); - + worker.use( + rest.get( + 'https://versions.backstage.io/v1/tags/main/manifest.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + packages: [], + }), + ), + ), + ); const { log: logs } = await withLogCollector(['log'], async () => { - await bump({ pattern: '@{backstage,backstage-extra}/*' } as any); + await bump({ + pattern: '@{backstage,backstage-extra}/*', + release: 'main', + } as any); }); expect(logs.filter(Boolean)).toEqual([ 'Using custom pattern glob @{backstage,backstage-extra}/*', - 'Checking for updates of @backstage/theme', - 'Checking for updates of @backstage-extra/custom-two', - 'Checking for updates of @backstage-extra/custom', 'Checking for updates of @backstage/core', + 'Checking for updates of @backstage-extra/custom', + 'Checking for updates of @backstage-extra/custom-two', + 'Checking for updates of @backstage/theme', 'Checking for updates of @backstage/core-api', 'Some packages are outdated, updating', - 'unlocking @backstage-extra/custom@^1.0.1 ~> 1.1.0', 'unlocking @backstage/core@^1.0.3 ~> 1.0.6', + 'unlocking @backstage-extra/custom@^1.0.1 ~> 1.1.0', 'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7', 'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7', 'bumping @backstage-extra/custom-two in a to ^2.0.0', - 'bumping @backstage/theme in b to ^2.0.0', 'bumping @backstage-extra/custom-two in b to ^2.0.0', + 'bumping @backstage/theme in b to ^2.0.0', + 'Skipping backstage.json update as custom pattern is used', 'Running yarn install to install new versions', '⚠️ The following packages may have breaking changes:', ' @backstage-extra/custom-two : 1.0.0 ~> 2.0.0', @@ -273,7 +655,7 @@ describe('bump', () => { 'Version bump complete!', ]); - expect(runObj.runPlain).toHaveBeenCalledTimes(6); + expect(runObj.runPlain).toHaveBeenCalledTimes(5); expect(runObj.runPlain).toHaveBeenCalledWith( 'yarn', 'info', @@ -288,7 +670,11 @@ describe('bump', () => { ); expect(runObj.run).toHaveBeenCalledTimes(1); - expect(runObj.run).toHaveBeenCalledWith('yarn', ['install']); + expect(runObj.run).toHaveBeenCalledWith( + 'yarn', + ['install'], + expect.any(Object), + ); const lockfileContents = await fs.readFile('/yarn.lock', 'utf8'); expect(lockfileContents).toEqual(customLockfileMockResult); @@ -342,20 +728,31 @@ describe('bump', () => { .mockImplementation((...path) => resolvePath('/', ...path)); jest.spyOn(runObj, 'runPlain').mockImplementation(async () => ''); jest.spyOn(runObj, 'run').mockResolvedValue(undefined); - + worker.use( + rest.get( + 'https://versions.backstage.io/v1/tags/main/manifest.json', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + packages: [], + }), + ), + ), + ); const { log: logs } = await withLogCollector(['log'], async () => { - await bump({ pattern: null } as unknown as Command); + await bump({ pattern: null, release: 'main' } as unknown as Command); }); expect(logs.filter(Boolean)).toEqual([ 'Using default pattern glob @backstage/*', - 'Checking for updates of @backstage/theme', 'Checking for updates of @backstage/core', - 'Package info not found, ignoring package @backstage/theme', - 'Package info not found, ignoring package @backstage/core', 'Checking for updates of @backstage/theme', - 'Checking for updates of @backstage/core', - 'Package info not found, ignoring package @backstage/theme', 'Package info not found, ignoring package @backstage/core', + 'Package info not found, ignoring package @backstage/theme', + 'Checking for updates of @backstage/core', + 'Checking for updates of @backstage/theme', + 'Package info not found, ignoring package @backstage/core', + 'Package info not found, ignoring package @backstage/theme', 'All Backstage packages are up to date!', ]); @@ -393,27 +790,20 @@ describe('bumpBackstageJsonVersion', () => { '/backstage.json': JSON.stringify({ version: '0.0.1' }), }); paths.targetDir = '/'; - const latest = '1.4.1'; jest .spyOn(paths, 'resolveTargetRoot') .mockImplementation((...path) => resolvePath('/', ...path)); - jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) => - JSON.stringify({ - type: 'inspect', - data: { - name, - 'dist-tags': { - latest, - }, - }, - }), - ); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); - await bumpBackstageJsonVersion(); - - const json = await fs.readJson('/backstage.json'); - expect(json).toEqual({ version: '1.4.1' }); + const { log } = await withLogCollector(async () => { + await bumpBackstageJsonVersion('1.4.1'); + }); + expect(await fs.readJson('/backstage.json')).toEqual({ version: '1.4.1' }); + expect(log).toEqual([ + 'Upgraded from release 0.0.1 to 1.4.1, please review these template changes:', + undefined, + ' https://backstage.github.io/upgrade-helper/?from=0.0.1&to=1.4.1', + undefined, + ]); }); it("should create backstage.json if doesn't exist", async () => { @@ -423,22 +813,105 @@ describe('bumpBackstageJsonVersion', () => { jest .spyOn(paths, 'resolveTargetRoot') .mockImplementation((...path) => resolvePath('/', ...path)); - jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) => - JSON.stringify({ - type: 'inspect', - data: { - name, - 'dist-tags': { - latest, - }, - }, - }), - ); - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); - await bumpBackstageJsonVersion(); - - const json = await fs.readJson('/backstage.json'); - expect(json).toEqual({ version: '1.4.1' }); + const { log } = await withLogCollector(async () => { + await bumpBackstageJsonVersion(latest); + }); + expect(await fs.readJson('/backstage.json')).toEqual({ version: latest }); + expect(log).toEqual([ + 'Your project is now at version 1.4.1, which has been written to backstage.json', + ]); + }); +}); + +describe('createVersionFinder', () => { + async function findVersion(tag: string, data: Partial) { + const fetcher = () => + Promise.resolve({ + name: '@backstage/core', + 'dist-tags': {}, + versions: [], + time: {}, + ...data, + }); + + const versionFinder = createVersionFinder({ + releaseLine: tag, + packageInfoFetcher: fetcher, + }); + let result; + await withLogCollector(async () => { + result = await versionFinder('@backstage/core'); + }); + return result; + } + + it('should create version finder', async () => { + await expect( + findVersion('latest', { + time: { '1.0.0': '2020-01-01T00:00:00.000Z' }, + 'dist-tags': { latest: '1.0.0' }, + }), + ).resolves.toBe('1.0.0'); + + await expect( + findVersion('main', { + time: { '1.0.0': '2020-01-01T00:00:00.000Z' }, + 'dist-tags': { latest: '1.0.0' }, + }), + ).resolves.toBe('1.0.0'); + + await expect( + findVersion('next', { + time: { '1.0.0': '2020-01-01T00:00:00.000Z' }, + 'dist-tags': { latest: '1.0.0' }, + }), + ).resolves.toBe('1.0.0'); + + await expect( + findVersion('next', { + time: { + '1.0.0': '2020-01-01T00:00:00.000Z', + '0.9.0': '2010-01-01T00:00:00.000Z', + }, + 'dist-tags': { latest: '1.0.0', next: '0.9.0' }, + }), + ).resolves.toBe('1.0.0'); + + await expect( + findVersion('next', { + time: { + '1.0.0': '2020-01-01T00:00:00.000Z', + '0.9.0': '2020-02-01T00:00:00.000Z', + }, + 'dist-tags': { latest: '1.0.0', next: '0.9.0' }, + }), + ).resolves.toBe('0.9.0'); + + await expect(findVersion('next', {})).rejects.toThrow( + "No target 'latest' version found for @backstage/core", + ); + + await expect( + findVersion('next', { + time: { + '0.9.0': '2020-02-01T00:00:00.000Z', + }, + 'dist-tags': { latest: '1.0.0', next: '0.9.0' }, + }), + ).rejects.toThrow( + "No time available for version '1.0.0' of @backstage/core", + ); + + await expect( + findVersion('next', { + time: { + '1.0.0': '2020-01-01T00:00:00.000Z', + }, + 'dist-tags': { latest: '1.0.0', next: '0.9.0' }, + }), + ).rejects.toThrow( + "No time available for version '0.9.0' of @backstage/core", + ); }); }); diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 2c16f324bc..b9d318bccc 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -16,10 +16,11 @@ import fs from 'fs-extra'; import chalk from 'chalk'; +import ora from 'ora'; import semver from 'semver'; import minimatch from 'minimatch'; import { Command } from 'commander'; -import { isError } from '@backstage/errors'; +import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; import { run } from '../../lib/run'; import { paths } from '../../lib/paths'; @@ -27,9 +28,16 @@ import { mapDependencies, fetchPackageInfo, Lockfile, + YarnInfoInspectData, } from '../../lib/versioning'; import { forbiddenDuplicatesFilter } from './lint'; import { BACKSTAGE_JSON } from '@backstage/cli-common'; +import { runParallelWorkers } from '../../lib/parallel'; +import { + getManifestByReleaseLine, + getManifestByVersion, + ReleaseManifest, +} from '@backstage/release-manifests'; const DEP_TYPES = [ 'dependencies', @@ -59,7 +67,37 @@ export default async (cmd: Command) => { console.log(`Using custom pattern glob ${pattern}`); } - const findTargetVersion = createVersionFinder(); + let findTargetVersion: (name: string) => Promise; + let releaseManifest: ReleaseManifest; + // Specific release specified. Be strict when resolving versions + if (semver.valid(cmd.release)) { + releaseManifest = await getManifestByVersion({ version: cmd.release }); + findTargetVersion = createStrictVersionFinder({ + releaseManifest, + }); + } else { + // Release line specified. Be lenient when resolving versions. + if (cmd.release === 'next') { + const next = await getManifestByReleaseLine({ + releaseLine: 'next', + }); + const main = await getManifestByReleaseLine({ + releaseLine: 'main', + }); + // Prefer manifest with the latest release version + releaseManifest = semver.gt(next.releaseVersion, main.releaseVersion) + ? next + : main; + } else { + releaseManifest = await getManifestByReleaseLine({ + releaseLine: cmd.release, + }); + } + findTargetVersion = createVersionFinder({ + releaseLine: cmd.releaseLine, + releaseManifest, + }); + } // First we discover all Backstage dependencies within our own repo const dependencyMap = await mapDependencies(paths.targetDir, pattern); @@ -68,67 +106,76 @@ export default async (cmd: Command) => { const versionBumps = new Map(); // Track package versions that we want to remove from yarn.lock in order to trigger a bump const unlocked = Array<{ name: string; range: string; target: string }>(); - await workerThreads(16, dependencyMap.entries(), async ([name, pkgs]) => { - let target: string; - try { - target = await findTargetVersion(name); - } catch (error) { - if (isError(error) && error.name === 'NotFoundError') { - console.log(`Package info not found, ignoring package ${name}`); - return; - } - throw error; - } - for (const pkg of pkgs) { - if (semver.satisfies(target, pkg.range)) { - if (semver.minVersion(pkg.range)?.version !== target) { - unlocked.push({ name, range: pkg.range, target }); + await runParallelWorkers({ + parallelismFactor: 4, + items: dependencyMap.entries(), + async worker([name, pkgs]) { + let target: string; + try { + target = await findTargetVersion(name); + } catch (error) { + if (isError(error) && error.name === 'NotFoundError') { + console.log(`Package info not found, ignoring package ${name}`); + return; } - - continue; + throw error; } - versionBumps.set( - pkg.name, - (versionBumps.get(pkg.name) ?? []).concat({ - name, - location: pkg.location, - range: `^${target}`, // TODO(Rugvip): Option to use something else than ^? - target, - }), - ); - } + + for (const pkg of pkgs) { + if (semver.satisfies(target, pkg.range)) { + if (semver.minVersion(pkg.range)?.version !== target) { + unlocked.push({ name, range: pkg.range, target }); + } + + continue; + } + versionBumps.set( + pkg.name, + (versionBumps.get(pkg.name) ?? []).concat({ + name, + location: pkg.location, + range: `^${target}`, // TODO(Rugvip): Option to use something else than ^? + target, + }), + ); + } + }, }); const filter = (name: string) => minimatch(name, pattern); // Check for updates of transitive backstage dependencies - await workerThreads(16, lockfile.keys(), async name => { - // Only check @backstage packages and friends, we don't want this to do a full update of all deps - if (!filter(name)) { - return; - } - - let target: string; - try { - target = await findTargetVersion(name); - } catch (error) { - if (isError(error) && error.name === 'NotFoundError') { - console.log(`Package info not found, ignoring package ${name}`); + await runParallelWorkers({ + parallelismFactor: 4, + items: lockfile.keys(), + async worker(name) { + // Only check @backstage packages and friends, we don't want this to do a full update of all deps + if (!filter(name)) { return; } - throw error; - } - for (const entry of lockfile.get(name) ?? []) { - // Ignore lockfile entries that don't satisfy the version range, since - // these can't cause the package to be locked to an older version - if (!semver.satisfies(target, entry.range)) { - continue; + let target: string; + try { + target = await findTargetVersion(name); + } catch (error) { + if (isError(error) && error.name === 'NotFoundError') { + console.log(`Package info not found, ignoring package ${name}`); + return; + } + throw error; } - // Unlock all entries that are within range but on the old version - unlocked.push({ name, range: entry.range, target }); - } + + for (const entry of lockfile.get(name) ?? []) { + // Ignore lockfile entries that don't satisfy the version range, since + // these can't cause the package to be locked to an older version + if (!semver.satisfies(target, entry.range)) { + continue; + } + // Unlock all entries that are within range but on the old version + unlocked.push({ name, range: entry.range, target }); + } + }, }); console.log(); @@ -163,50 +210,58 @@ export default async (cmd: Command) => { } const breakingUpdates = new Map(); - await workerThreads(16, versionBumps.entries(), async ([name, deps]) => { - const pkgPath = resolvePath(deps[0].location, 'package.json'); - const pkgJson = await fs.readJson(pkgPath); + await runParallelWorkers({ + parallelismFactor: 4, + items: versionBumps.entries(), + async worker([name, deps]) { + const pkgPath = resolvePath(deps[0].location, 'package.json'); + const pkgJson = await fs.readJson(pkgPath); - for (const dep of deps) { - console.log( - `${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan( - name, - )} to ${chalk.yellow(dep.range)}`, - ); + for (const dep of deps) { + console.log( + `${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan( + name, + )} to ${chalk.yellow(dep.range)}`, + ); - for (const depType of DEP_TYPES) { - if (depType in pkgJson && dep.name in pkgJson[depType]) { - const oldRange = pkgJson[depType][dep.name]; - pkgJson[depType][dep.name] = dep.range; + for (const depType of DEP_TYPES) { + if (depType in pkgJson && dep.name in pkgJson[depType]) { + const oldRange = pkgJson[depType][dep.name]; + pkgJson[depType][dep.name] = dep.range; - // Check if the update was at least a pre-v1 minor or post-v1 major release - const lockfileEntry = lockfile - .get(dep.name) - ?.find(entry => entry.range === oldRange); - if (lockfileEntry) { - const from = lockfileEntry.version; - const to = dep.target; - if (!semver.satisfies(to, `^${from}`)) { - breakingUpdates.set(dep.name, { from, to }); + // Check if the update was at least a pre-v1 minor or post-v1 major release + const lockfileEntry = lockfile + .get(dep.name) + ?.find(entry => entry.range === oldRange); + if (lockfileEntry) { + const from = lockfileEntry.version; + const to = dep.target; + if (!semver.satisfies(to, `^${from}`)) { + breakingUpdates.set(dep.name, { from, to }); + } } } } } - } - await fs.writeJson(pkgPath, pkgJson, { spaces: 2 }); + await fs.writeJson(pkgPath, pkgJson, { spaces: 2 }); + }, }); console.log(); - await bumpBackstageJsonVersion(); + // Do not update backstage.json when upgrade patterns are used. + if (pattern === DEFAULT_PATTERN_GLOB) { + await bumpBackstageJsonVersion(releaseManifest.releaseVersion); + } else { + console.log( + chalk.yellow( + `Skipping backstage.json update as custom pattern is used`, + ), + ); + } - console.log(); - console.log( - `Running ${chalk.blue('yarn install')} to install new versions`, - ); - console.log(); - await run('yarn', ['install']); + await runYarnInstall(); if (breakingUpdates.size > 0) { console.log(); @@ -270,9 +325,38 @@ export default async (cmd: Command) => { } }; -function createVersionFinder() { - const found = new Map(); +export function createStrictVersionFinder(options: { + releaseManifest: ReleaseManifest; +}) { + const releasePackages = new Map( + options.releaseManifest.packages.map(p => [p.name, p.version]), + ); + return async function findTargetVersion(name: string) { + console.log(`Checking for updates of ${name}`); + const manifestVersion = releasePackages.get(name); + if (manifestVersion) { + return manifestVersion; + } + throw new NotFoundError(`Package ${name} not found in release manifest`); + }; +} +export function createVersionFinder(options: { + releaseLine?: string; + packageInfoFetcher?: () => Promise; + releaseManifest?: ReleaseManifest; +}) { + const { + releaseLine = 'latest', + packageInfoFetcher = fetchPackageInfo, + releaseManifest, + } = options; + // The main release line is just an alias for latest + const distTag = releaseLine === 'main' ? 'latest' : releaseLine; + const found = new Map(); + const releasePackages = new Map( + releaseManifest?.packages.map(p => [p.name, p.version]), + ); return async function findTargetVersion(name: string) { const existing = found.get(name); if (existing) { @@ -280,17 +364,50 @@ function createVersionFinder() { } console.log(`Checking for updates of ${name}`); - const info = await fetchPackageInfo(name); - const latest = info['dist-tags'].latest; - if (!latest) { - throw new Error(`No latest version found for ${name}`); + const manifestVersion = releasePackages.get(name); + if (manifestVersion) { + return manifestVersion; } - found.set(name, latest); - return latest; + + const info = await packageInfoFetcher(name); + const latestVersion = info['dist-tags'].latest; + if (!latestVersion) { + throw new Error(`No target 'latest' version found for ${name}`); + } + + const taggedVersion = info['dist-tags'][distTag]; + if (distTag === 'latest' || !taggedVersion) { + found.set(name, latestVersion); + return latestVersion; + } + + const latestVersionDateStr = info.time[latestVersion]; + const taggedVersionDateStr = info.time[taggedVersion]; + if (!latestVersionDateStr) { + throw new Error( + `No time available for version '${latestVersion}' of ${name}`, + ); + } + if (!taggedVersionDateStr) { + throw new Error( + `No time available for version '${taggedVersion}' of ${name}`, + ); + } + + const latestVersionRelease = new Date(latestVersionDateStr).getTime(); + const taggedVersionRelease = new Date(taggedVersionDateStr).getTime(); + if (latestVersionRelease > taggedVersionRelease) { + // Prefer latest version if it's newer. + found.set(name, latestVersion); + return latestVersion; + } + + found.set(name, taggedVersion); + return taggedVersion; }; } -export async function bumpBackstageJsonVersion() { +export async function bumpBackstageJsonVersion(version: string) { const backstageJsonPath = paths.resolveTargetRoot(BACKSTAGE_JSON); const backstageJson = await fs.readJSON(backstageJsonPath).catch(e => { if (e.code === 'ENOENT') { @@ -300,24 +417,38 @@ export async function bumpBackstageJsonVersion() { throw e; }); - const info = await fetchPackageInfo('@backstage/create-app'); - const { latest } = info['dist-tags']; + const prevVersion = backstageJson?.version; - if (backstageJson?.version === latest) { + if (prevVersion === version) { return; } - console.log( - chalk.yellow( - typeof backstageJson === 'undefined' - ? `Creating ${BACKSTAGE_JSON}` - : `Bumping version in ${BACKSTAGE_JSON}`, - ), - ); + const { yellow, cyan, green } = chalk; + if (prevVersion) { + const from = encodeURIComponent(prevVersion); + const to = encodeURIComponent(version); + const link = `https://backstage.github.io/upgrade-helper/?from=${from}&to=${to}`; + console.log( + yellow( + `Upgraded from release ${green(prevVersion)} to ${green( + version, + )}, please review these template changes:`, + ), + ); + console.log(); + console.log(` ${cyan(link)}`); + console.log(); + } else { + console.log( + yellow( + `Your project is now at version ${version}, which has been written to ${BACKSTAGE_JSON}`, + ), + ); + } await fs.writeJson( backstageJsonPath, - { ...backstageJson, version: latest }, + { ...backstageJson, version }, { spaces: 2, encoding: 'utf8', @@ -325,26 +456,34 @@ export async function bumpBackstageJsonVersion() { ); } -async function workerThreads( - count: number, - items: IterableIterator, - fn: (item: T) => Promise, -) { - const queue = Array.from(items); +async function runYarnInstall() { + const spinner = ora({ + prefixText: `Running ${chalk.blue('yarn install')} to install new versions`, + spinner: 'arc', + color: 'green', + }).start(); - async function pop() { - const item = queue.pop(); - if (!item) { - return; - } - - await fn(item); - await pop(); + const installOutput = new Array(); + try { + await run('yarn', ['install'], { + env: { + FORCE_COLOR: 'true', + // We filter out all of the npm_* environment variables that are added when + // executing through yarn. This works around an issue where these variables + // incorrectly override local yarn or npm config in the project directory. + ...Object.fromEntries( + Object.entries(process.env).map(([name, value]) => + name.startsWith('npm_') ? [name, undefined] : [name, value], + ), + ), + }, + stdoutLogFunc: data => installOutput.push(data), + stderrLogFunc: data => installOutput.push(data), + }); + spinner.succeed(); + } catch (error) { + spinner.fail(); + process.stdout.write(Buffer.concat(installOutput)); + throw error; } - - return Promise.all( - Array(count) - .fill(0) - .map(() => pop()), - ); } diff --git a/packages/cli/src/lib/builder/buildTypeDefinitions.ts b/packages/cli/src/lib/builder/buildTypeDefinitions.ts index ba67532bdb..3bf3a4495f 100644 --- a/packages/cli/src/lib/builder/buildTypeDefinitions.ts +++ b/packages/cli/src/lib/builder/buildTypeDefinitions.ts @@ -16,114 +16,89 @@ import fs from 'fs-extra'; import chalk from 'chalk'; -import { - relative as relativePath, - resolve as resolvePath, - dirname, -} from 'path'; +import { relative as relativePath, resolve as resolvePath } from 'path'; import { paths } from '../paths'; +import { buildTypeDefinitionsWorker } from './buildTypeDefinitionsWorker'; +import { runWorkerThreads } from '../parallel'; // These message types are ignored since we want to avoid duplicating the logic of // handling them correctly, and we already have the API Reports warning about them. const ignoredMessages = new Set(['tsdoc-undefined-tag', 'ae-forgotten-export']); -let apiExtractor: undefined | typeof import('@microsoft/api-extractor'); -function prepareApiExtractor() { - if (apiExtractor) { - return apiExtractor; - } - - try { - apiExtractor = require('@microsoft/api-extractor'); - } catch (error) { - throw new Error( - 'Failed to resolve @microsoft/api-extractor, it must best installed ' + - 'as a dependency of your project in order to use experimental type builds', - ); - } - - /** - * All of this monkey patching below is because MUI has these bare package.json file as a method - * for making TypeScript accept imports like `@material-ui/core/Button`, and improve tree-shaking - * by declaring them side effect free. - * - * The package.json lookup logic in api-extractor really doesn't like that though, as it enforces - * that the 'name' field exists in all package.json files that it discovers. This below is just - * making sure that we ignore those file package.json files instead of crashing. - */ - const { - PackageJsonLookup, - // eslint-disable-next-line import/no-extraneous-dependencies - } = require('@rushstack/node-core-library/lib/PackageJsonLookup'); - - const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor; - PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor = - function tryGetPackageJsonFilePathForPatch(path: string) { - if ( - path.includes('@material-ui') && - !dirname(path).endsWith('@material-ui') - ) { - return undefined; - } - return old.call(this, path); - }; - - return apiExtractor!; -} - -export async function buildTypeDefinitions() { - const { Extractor, ExtractorConfig } = prepareApiExtractor(); - - const distTypesPackageDir = paths.resolveTargetRoot( - 'dist-types', - relativePath(paths.targetRoot, paths.targetDir), +export async function buildTypeDefinitions( + targetDirs: string[] = [paths.targetDir], +) { + const packageDirs = targetDirs.map(dir => + relativePath(paths.targetRoot, dir), ); - const entryPoint = resolvePath(distTypesPackageDir, 'src/index.d.ts'); + const entryPoints = await Promise.all( + packageDirs.map(async dir => { + const entryPoint = paths.resolveTargetRoot( + 'dist-types', + dir, + 'src/index.d.ts', + ); - const declarationsExist = await fs.pathExists(entryPoint); - if (!declarationsExist) { - const path = relativePath(paths.targetDir, entryPoint); - throw new Error( - `No declaration files found at ${path}, be sure to run ${chalk.bgRed.white( - 'yarn tsc', - )} to generate .d.ts files before packaging`, - ); - } + const declarationsExist = await fs.pathExists(entryPoint); + if (!declarationsExist) { + throw new Error( + `No declaration files found at ${entryPoint}, be sure to run ${chalk.bgRed.white( + 'yarn tsc', + )} to generate .d.ts files before packaging`, + ); + } + return entryPoint; + }), + ); - const extractorConfig = ExtractorConfig.prepare({ - configObject: { - mainEntryPointFilePath: entryPoint, - bundledPackages: [], + const workerConfigs = packageDirs.map(packageDir => { + const targetDir = paths.resolveTargetRoot(packageDir); + const targetTypesDir = paths.resolveTargetRoot('dist-types', packageDir); + const extractorOptions = { + configObject: { + mainEntryPointFilePath: resolvePath(targetTypesDir, 'src/index.d.ts'), + bundledPackages: [], - compiler: { - skipLibCheck: true, - tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'), + compiler: { + skipLibCheck: true, + tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'), + }, + + dtsRollup: { + enabled: true, + untrimmedFilePath: resolvePath(targetDir, 'dist/index.alpha.d.ts'), + betaTrimmedFilePath: resolvePath(targetDir, 'dist/index.beta.d.ts'), + publicTrimmedFilePath: resolvePath(targetDir, 'dist/index.d.ts'), + }, + + newlineKind: 'lf', + + projectFolder: targetDir, }, - - dtsRollup: { - enabled: true, - untrimmedFilePath: paths.resolveTarget('dist/index.alpha.d.ts'), - betaTrimmedFilePath: paths.resolveTarget('dist/index.beta.d.ts'), - publicTrimmedFilePath: paths.resolveTarget('dist/index.d.ts'), - }, - - newlineKind: 'lf', - - projectFolder: paths.targetDir, - }, - configObjectFullPath: paths.targetDir, - packageJsonFullPath: paths.resolveTarget('package.json'), + configObjectFullPath: targetDir, + packageJsonFullPath: resolvePath(targetDir, 'package.json'), + }; + return { extractorOptions, targetTypesDir }; }); const typescriptDir = paths.resolveTargetRoot('node_modules/typescript'); const hasTypescript = await fs.pathExists(typescriptDir); - const extractorResult = Extractor.invoke(extractorConfig, { - typescriptCompilerFolder: hasTypescript ? typescriptDir : undefined, - localBuild: false, - showVerboseMessages: false, - showDiagnostics: false, - messageCallback(message) { - message.handled = true; + const typescriptCompilerFolder = hasTypescript ? typescriptDir : undefined; + await runWorkerThreads({ + threadCount: 1, + workerData: { + entryPoints, + workerConfigs, + typescriptCompilerFolder, + }, + worker: buildTypeDefinitionsWorker, + onMessage: ({ + message, + targetTypesDir, + }: { + message: any; + targetTypesDir: string; + }) => { if (ignoredMessages.has(message.messageId)) { return; } @@ -131,7 +106,7 @@ export async function buildTypeDefinitions() { let text = `${message.text} (${message.messageId})`; if (message.sourceFilePath) { text += ' at '; - text += relativePath(distTypesPackageDir, message.sourceFilePath); + text += relativePath(targetTypesDir, message.sourceFilePath); if (message.sourceFileLine) { text += `:${message.sourceFileLine}`; if (message.sourceFileColumn) { @@ -151,11 +126,4 @@ export async function buildTypeDefinitions() { } }, }); - - if (!extractorResult.succeeded) { - throw new Error( - `Type definition build completed with ${extractorResult.errorCount} errors` + - ` and ${extractorResult.warningCount} warnings`, - ); - } } diff --git a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts new file mode 100644 index 0000000000..43e5cbc526 --- /dev/null +++ b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * NOTE: This is a worker thread function that is stringified and executed + * withing a `worker_threads.Worker`. Everything in this function must + * be self-contained. + * Using TypeScript is fine as it is transpiled before being stringified. + */ +export async function buildTypeDefinitionsWorker( + workerData: any, + sendMessage: (message: any) => void, +) { + try { + require('@microsoft/api-extractor'); + } catch (error) { + throw new Error( + 'Failed to resolve @microsoft/api-extractor, it must best installed ' + + 'as a dependency of your project in order to use experimental type builds', + ); + } + + const { dirname } = require('path'); + const { entryPoints, workerConfigs, typescriptCompilerFolder } = workerData; + + const apiExtractor = require('@microsoft/api-extractor'); + const { Extractor, ExtractorConfig, CompilerState } = apiExtractor; + + /** + * All of this monkey patching below is because MUI has these bare package.json file as a method + * for making TypeScript accept imports like `@material-ui/core/Button`, and improve tree-shaking + * by declaring them side effect free. + * + * The package.json lookup logic in api-extractor really doesn't like that though, as it enforces + * that the 'name' field exists in all package.json files that it discovers. This below is just + * making sure that we ignore those file package.json files instead of crashing. + */ + const { + PackageJsonLookup, + // eslint-disable-next-line import/no-extraneous-dependencies + } = require('@rushstack/node-core-library/lib/PackageJsonLookup'); + + const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor; + PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor = + function tryGetPackageJsonFilePathForPatch(path: string) { + if ( + path.includes('@material-ui') && + !dirname(path).endsWith('@material-ui') + ) { + return undefined; + } + return old.call(this, path); + }; + + let compilerState; + for (const { extractorOptions, targetTypesDir } of workerConfigs) { + const extractorConfig = ExtractorConfig.prepare(extractorOptions); + + if (!compilerState) { + compilerState = CompilerState.create(extractorConfig, { + additionalEntryPoints: entryPoints, + }); + } + + const extractorResult = Extractor.invoke(extractorConfig, { + compilerState, + localBuild: false, + typescriptCompilerFolder, + showVerboseMessages: false, + showDiagnostics: false, + messageCallback: (message: any) => { + message.handled = true; + sendMessage({ message, targetTypesDir }); + }, + }); + + if (!extractorResult.succeeded) { + throw new Error( + `Type definition build completed with ${extractorResult.errorCount} errors` + + ` and ${extractorResult.warningCount} warnings`, + ); + } + } +} diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 617e354864..0a214f1a7a 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -16,7 +16,7 @@ import chalk from 'chalk'; import fs from 'fs-extra'; -import { relative as relativePath } from 'path'; +import { relative as relativePath, resolve as resolvePath } from 'path'; import peerDepsExternal from 'rollup-plugin-peer-deps-external'; import commonjs from '@rollup/plugin-commonjs'; import resolve from '@rollup/plugin-node-resolve'; @@ -26,7 +26,7 @@ import svgr from '@svgr/rollup'; import dts from 'rollup-plugin-dts'; import json from '@rollup/plugin-json'; import yaml from '@rollup/plugin-yaml'; -import { RollupOptions, OutputOptions } from 'rollup'; +import { RollupOptions, OutputOptions, RollupWarning } from 'rollup'; import { forwardFileImports } from './plugins'; import { BuildOptions, Output } from './types'; @@ -37,6 +37,19 @@ export async function makeRollupConfigs( options: BuildOptions, ): Promise { const configs = new Array(); + const targetDir = options.targetDir ?? paths.targetDir; + const onwarn = ({ code, message }: RollupWarning) => { + if (code === 'EMPTY_BUNDLE') { + return; // We don't care about this one + } + if (options.logPrefix) { + console.log(options.logPrefix + message); + } else { + console.log(message); + } + }; + + const distDir = resolvePath(targetDir, 'dist'); if (options.outputs.has(Output.cjs) || options.outputs.has(Output.esm)) { const output = new Array(); @@ -44,7 +57,7 @@ export async function makeRollupConfigs( if (options.outputs.has(Output.cjs)) { output.push({ - dir: 'dist', + dir: distDir, entryFileNames: 'index.cjs.js', chunkFileNames: 'cjs/[name]-[hash].cjs.js', format: 'commonjs', @@ -53,7 +66,7 @@ export async function makeRollupConfigs( } if (options.outputs.has(Output.esm)) { output.push({ - dir: 'dist', + dir: distDir, entryFileNames: 'index.esm.js', chunkFileNames: 'esm/[name]-[hash].esm.js', format: 'module', @@ -64,12 +77,14 @@ export async function makeRollupConfigs( } configs.push({ - input: 'src/index.ts', + input: resolvePath(targetDir, 'src/index.ts'), output, + onwarn, preserveEntrySignatures: 'strict', external: require('module').builtinModules, plugins: [ peerDepsExternal({ + packageJsonPath: resolvePath(targetDir, 'package.json'), includeDependencies: true, }), resolve({ mainFields }), @@ -109,13 +124,13 @@ export async function makeRollupConfigs( if (options.outputs.has(Output.types) && !options.useApiExtractor) { const typesInput = paths.resolveTargetRoot( 'dist-types', - relativePath(paths.targetRoot, paths.targetDir), + relativePath(paths.targetRoot, targetDir), 'src/index.d.ts', ); const declarationsExist = await fs.pathExists(typesInput); if (!declarationsExist) { - const path = relativePath(paths.targetDir, typesInput); + const path = relativePath(targetDir, typesInput); throw new Error( `No declaration files found at ${path}, be sure to run ${chalk.bgRed.white( 'yarn tsc', @@ -126,9 +141,10 @@ export async function makeRollupConfigs( configs.push({ input: typesInput, output: { - file: 'dist/index.d.ts', + file: resolvePath(distDir, 'index.d.ts'), format: 'es', }, + onwarn, plugins: [dts()], }); } diff --git a/packages/cli/src/lib/builder/index.ts b/packages/cli/src/lib/builder/index.ts index c39d964ade..00cc463cfb 100644 --- a/packages/cli/src/lib/builder/index.ts +++ b/packages/cli/src/lib/builder/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { buildPackage } from './packager'; +export { buildPackage, buildPackages, getOutputsForRole } from './packager'; export { Output } from './types'; export type { BuildOptions } from './types'; diff --git a/packages/cli/src/lib/builder/packager.ts b/packages/cli/src/lib/builder/packager.ts index 96c488fce2..c2256c8417 100644 --- a/packages/cli/src/lib/builder/packager.ts +++ b/packages/cli/src/lib/builder/packager.ts @@ -17,11 +17,13 @@ import fs from 'fs-extra'; import { rollup, RollupOptions } from 'rollup'; import chalk from 'chalk'; -import { relative as relativePath } from 'path'; +import { relative as relativePath, resolve as resolvePath } from 'path'; import { paths } from '../paths'; import { makeRollupConfigs } from './config'; import { BuildOptions, Output } from './types'; import { buildTypeDefinitions } from './buildTypeDefinitions'; +import { getRoleInfo } from '../role'; +import { runParallelWorkers } from '../parallel'; export function formatErrorMessage(error: any) { let msg = ''; @@ -116,3 +118,51 @@ export const buildPackage = async (options: BuildOptions) => { await Promise.all(buildTasks); }; + +export const buildPackages = async (options: BuildOptions[]) => { + if (options.some(opt => !opt.targetDir)) { + throw new Error('targetDir must be set for all build options'); + } + const rollupConfigs = await Promise.all(options.map(makeRollupConfigs)); + + await Promise.all( + options.map(({ targetDir }) => fs.remove(resolvePath(targetDir!, 'dist'))), + ); + + const buildTasks = rollupConfigs.flat().map(opts => () => rollupBuild(opts)); + + const typeDefinitionTargetDirs = options + .filter( + ({ outputs, useApiExtractor }) => + outputs.has(Output.types) && useApiExtractor, + ) + .map(_ => _.targetDir!); + + if (typeDefinitionTargetDirs.length > 0) { + // Make sure this one is started first + buildTasks.unshift(() => buildTypeDefinitions(typeDefinitionTargetDirs)); + } + + await runParallelWorkers({ + items: buildTasks, + worker: async task => task(), + }); +}; + +export function getOutputsForRole(role: string): Set { + const outputs = new Set(); + + for (const output of getRoleInfo(role).output) { + if (output === 'cjs') { + outputs.add(Output.cjs); + } + if (output === 'esm') { + outputs.add(Output.esm); + } + if (output === 'types') { + outputs.add(Output.types); + } + } + + return outputs; +} diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/lib/builder/types.ts index a02afef01a..f43d84cd92 100644 --- a/packages/cli/src/lib/builder/types.ts +++ b/packages/cli/src/lib/builder/types.ts @@ -21,6 +21,8 @@ export enum Output { } export type BuildOptions = { + logPrefix?: string; + targetDir?: string; outputs: Set; minify?: boolean; useApiExtractor?: boolean; diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index bd2d27a2b8..3cfcafc005 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -93,7 +93,7 @@ export async function createConfig( if (checksEnabled) { plugins.push( new ForkTsCheckerWebpackPlugin({ - typescript: { configFile: paths.targetTsConfig }, + typescript: { configFile: paths.targetTsConfig, memoryLimit: 4096 }, }), new ESLintPlugin({ context: paths.targetPath, diff --git a/packages/cli/src/lib/bundler/optimization.ts b/packages/cli/src/lib/bundler/optimization.ts index a5cf38fc11..0a5ef178cb 100644 --- a/packages/cli/src/lib/bundler/optimization.ts +++ b/packages/cli/src/lib/bundler/optimization.ts @@ -29,6 +29,7 @@ export const optimization = ( minimizer: [ new ESBuildMinifyPlugin({ target: 'es2019', + format: 'iife', }), ], runtimeChunk: 'single', diff --git a/packages/cli/src/lib/bundler/paths.ts b/packages/cli/src/lib/bundler/paths.ts index d465b3010f..3d4925c66b 100644 --- a/packages/cli/src/lib/bundler/paths.ts +++ b/packages/cli/src/lib/bundler/paths.ts @@ -15,55 +15,58 @@ */ import fs from 'fs-extra'; +import { resolve as resolvePath } from 'path'; import { paths } from '../paths'; export type BundlingPathsOptions = { // bundle entrypoint, e.g. 'src/index' entry: string; + // Target directory, defaulting to paths.targetDir + targetDir?: string; }; export function resolveBundlingPaths(options: BundlingPathsOptions) { - const { entry } = options; + const { entry, targetDir = paths.targetDir } = options; const resolveTargetModule = (pathString: string) => { for (const ext of ['mjs', 'js', 'ts', 'tsx', 'jsx']) { - const filePath = paths.resolveTarget(`${pathString}.${ext}`); + const filePath = resolvePath(targetDir, `${pathString}.${ext}`); if (fs.pathExistsSync(filePath)) { return filePath; } } - return paths.resolveTarget(`${pathString}.js`); + return resolvePath(targetDir, `${pathString}.js`); }; let targetPublic = undefined; - let targetHtml = paths.resolveTarget('public/index.html'); + let targetHtml = resolvePath(targetDir, 'public/index.html'); // Prefer public folder if (fs.pathExistsSync(targetHtml)) { - targetPublic = paths.resolveTarget('public'); + targetPublic = resolvePath(targetDir, 'public'); } else { - targetHtml = paths.resolveTarget(`${entry}.html`); + targetHtml = resolvePath(targetDir, `${entry}.html`); if (!fs.pathExistsSync(targetHtml)) { targetHtml = paths.resolveOwn('templates/serve_index.html'); } } // Backend plugin dev run file - const targetRunFile = paths.resolveTarget('src/run.ts'); + const targetRunFile = resolvePath(targetDir, 'src/run.ts'); const runFileExists = fs.pathExistsSync(targetRunFile); return { targetHtml, targetPublic, - targetPath: paths.resolveTarget('.'), + targetPath: resolvePath(targetDir, '.'), targetRunFile: runFileExists ? targetRunFile : undefined, - targetDist: paths.resolveTarget('dist'), - targetAssets: paths.resolveTarget('assets'), - targetSrc: paths.resolveTarget('src'), - targetDev: paths.resolveTarget('dev'), + targetDist: resolvePath(targetDir, 'dist'), + targetAssets: resolvePath(targetDir, 'assets'), + targetSrc: resolvePath(targetDir, 'src'), + targetDev: resolvePath(targetDir, 'dev'), targetEntry: resolveTargetModule(entry), targetTsConfig: paths.resolveTargetRoot('tsconfig.json'), - targetPackageJson: paths.resolveTarget('package.json'), + targetPackageJson: resolvePath(targetDir, 'package.json'), rootNodeModules: paths.resolveTargetRoot('node_modules'), root: paths.targetRoot, }; diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index eae0452d8d..4d6d2e5c9e 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -16,7 +16,6 @@ import { AppConfig, Config } from '@backstage/config'; import { BundlingPathsOptions } from './paths'; -import { ParallelOption } from '../parallel'; import { ConfigSchema } from '@backstage/config-loader'; export type BundlingOptions = { @@ -25,7 +24,7 @@ export type BundlingOptions = { frontendConfig: Config; frontendAppConfigs: AppConfig[]; baseUrl: URL; - parallel?: ParallelOption; + parallelism?: number; }; export type ServeOptions = BundlingPathsOptions & { @@ -35,8 +34,10 @@ export type ServeOptions = BundlingPathsOptions & { }; export type BuildOptions = BundlingPathsOptions & { + // Target directory, defaulting to paths.targetDir + targetDir?: string; statsJsonEnabled: boolean; - parallel?: ParallelOption; + parallelism?: number; schema?: ConfigSchema; frontendConfig: Config; frontendAppConfigs: AppConfig[]; @@ -45,7 +46,7 @@ export type BuildOptions = BundlingPathsOptions & { export type BackendBundlingOptions = { checksEnabled: boolean; isDev: boolean; - parallel?: ParallelOption; + parallelism?: number; inspectEnabled: boolean; inspectBrkEnabled: boolean; }; diff --git a/packages/cli/src/lib/logging.ts b/packages/cli/src/lib/logging.ts index 8745585d1e..59473d7f64 100644 --- a/packages/cli/src/lib/logging.ts +++ b/packages/cli/src/lib/logging.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export type LogFunc = (data: Buffer | string) => void; +export type LogFunc = (data: Buffer) => void; export type LogPipe = (dst: NodeJS.WriteStream) => LogFunc; export type LogOptions = { diff --git a/packages/cli/src/lib/monorepo/PackageGraph.ts b/packages/cli/src/lib/monorepo/PackageGraph.ts index 9860b92ef6..89d5f79cd2 100644 --- a/packages/cli/src/lib/monorepo/PackageGraph.ts +++ b/packages/cli/src/lib/monorepo/PackageGraph.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { Package } from '@manypkg/get-packages'; +import { getPackages, Package } from '@manypkg/get-packages'; +import { paths } from '../paths'; +import { PackageRole } from '../role'; type PackageJSON = Package['packageJson']; @@ -25,8 +27,17 @@ export interface ExtendedPackageJSON extends PackageJSON { // The `bundled` field is a field known within Backstage, it means // that the package bundles all of its dependencies in its build output. bundled?: boolean; + + backstage?: { + role?: PackageRole; + }; } +export type ExtendedPackage = { + dir: string; + packageJson: ExtendedPackageJSON; +}; + export type PackageGraphNode = { /** The name of the package */ name: string; @@ -47,6 +58,11 @@ export type PackageGraphNode = { }; export class PackageGraph extends Map { + static async listTargetPackages(): Promise { + const { packages } = await getPackages(paths.targetDir); + return packages as ExtendedPackage[]; + } + static fromPackages(packages: Package[]): PackageGraph { const graph = new PackageGraph(); diff --git a/packages/cli/src/lib/packager/copyPackageDist.ts b/packages/cli/src/lib/packager/copyPackageDist.ts new file mode 100644 index 0000000000..03ac361006 --- /dev/null +++ b/packages/cli/src/lib/packager/copyPackageDist.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import npmPackList from 'npm-packlist'; +import { join as joinPath, resolve as resolvePath } from 'path'; + +const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes']; + +// Writes e.g. alpha/package.json +async function writeReleaseStageEntrypoint( + pkg: any, + stage: 'alpha' | 'beta', + targetDir: string, +) { + await fs.ensureDir(resolvePath(targetDir, stage)); + await fs.writeJson( + resolvePath(targetDir, stage, 'package.json'), + { + name: pkg.name, + version: pkg.version, + main: (pkg.publishConfig.main || pkg.main) && '..', + module: (pkg.publishConfig.module || pkg.module) && '..', + browser: (pkg.publishConfig.browser || pkg.browser) && '..', + types: joinPath('..', pkg.publishConfig[`${stage}Types`]), + }, + { encoding: 'utf8', spaces: 2 }, + ); +} + +export async function copyPackageDist(packageDir: string, targetDir: string) { + const pkgPath = resolvePath(packageDir, 'package.json'); + const pkgContent = await fs.readFile(pkgPath, 'utf8'); + const pkg = JSON.parse(pkgContent); + + const publishConfig = pkg.publishConfig ?? {}; + for (const key of Object.keys(publishConfig)) { + if (!SKIPPED_KEYS.includes(key)) { + pkg[key] = publishConfig[key]; + } + } + + // We remove the dependencies from package.json of packages that are marked + // as bundled, so that yarn doesn't try to install them. + if (pkg.bundled) { + delete pkg.dependencies; + delete pkg.devDependencies; + delete pkg.peerDependencies; + delete pkg.optionalDependencies; + } + + // Write the modified package.json so that the file listing is correct + await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 }); + + // Lists all dist files, respecting .npmignore, files field in package.json, etc. + const filePaths = await npmPackList({ path: packageDir }); + + await fs.ensureDir(targetDir); + for (const filePath of filePaths.sort()) { + await fs.copy( + resolvePath(packageDir, filePath), + resolvePath(targetDir, filePath), + ); + } + + if (publishConfig.alphaTypes) { + await writeReleaseStageEntrypoint(pkg, 'alpha', targetDir); + } + if (publishConfig.betaTypes) { + await writeReleaseStageEntrypoint(pkg, 'beta', targetDir); + } + + // Restore package.json + await fs.writeFile(pkgPath, pkgContent, 'utf8'); +} diff --git a/packages/cli/src/lib/packager/createDistWorkspace.ts b/packages/cli/src/lib/packager/createDistWorkspace.ts new file mode 100644 index 0000000000..15ccda30c2 --- /dev/null +++ b/packages/cli/src/lib/packager/createDistWorkspace.ts @@ -0,0 +1,303 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import chalk from 'chalk'; +import fs from 'fs-extra'; +import { + join as joinPath, + resolve as resolvePath, + relative as relativePath, +} from 'path'; +import { tmpdir } from 'os'; +import tar, { CreateOptions } from 'tar'; +import partition from 'lodash/partition'; +import { paths } from '../paths'; +import { run } from '../run'; +import { + dependencies as cliDependencies, + devDependencies as cliDevDependencies, +} from '../../../package.json'; +import { PackageGraph, PackageGraphNode } from '../monorepo'; +import { + BuildOptions, + buildPackages, + getOutputsForRole, + Output, +} from '../builder'; +import { copyPackageDist } from './copyPackageDist'; + +// These packages aren't safe to pack in parallel since the CLI depends on them +const UNSAFE_PACKAGES = [ + ...Object.keys(cliDependencies), + ...Object.keys(cliDevDependencies), +]; + +type FileEntry = + | string + | { + src: string; + dest: string; + }; + +type Options = { + /** + * Target directory for the dist workspace, defaults to a temporary directory + */ + targetDir?: string; + + /** + * Files to copy into the target workspace. + * + * Defaults to ['yarn.lock', 'package.json']. + */ + files?: FileEntry[]; + + /** + * If set to true, the target packages are built before they are packaged into the workspace. + */ + buildDependencies?: boolean; + + /** + * When `buildDependencies` is set, this list of packages will not be built even if they are dependencies. + */ + buildExcludes?: string[]; + + /** + * Controls amount of parallelism in some build steps. + */ + parallelism?: number; + + /** + * If set, creates a skeleton tarball that contains all package.json files + * with the same structure as the workspace dir. + */ + skeleton?: 'skeleton.tar' | 'skeleton.tar.gz'; +}; + +/** + * Uses `yarn pack` to package local packages and unpacks them into a dist workspace. + * The target workspace will end up containing dist version of each package and + * will be suitable for packaging e.g. into a docker image. + * + * This creates a structure that is functionally similar to if the packages were + * installed from npm, but uses Yarn workspaces to link to them at runtime. + */ +export async function createDistWorkspace( + packageNames: string[], + options: Options = {}, +) { + const targetDir = + options.targetDir ?? + (await fs.mkdtemp(resolvePath(tmpdir(), 'dist-workspace'))); + + const packages = await PackageGraph.listTargetPackages(); + const packageGraph = PackageGraph.fromPackages(packages); + const targetNames = packageGraph.collectPackageNames(packageNames, node => { + // Don't include dependencies of packages that are marked as bundled + if (node.packageJson.bundled) { + return undefined; + } + + return node.publishedLocalDependencies.keys(); + }); + const targets = Array.from(targetNames).map(name => packageGraph.get(name)!); + + if (options.buildDependencies) { + const exclude = options.buildExcludes ?? []; + + const toBuild = new Set( + targets.map(_ => _.name).filter(name => !exclude.includes(name)), + ); + + const standardBuilds = new Array(); + const customBuild = new Array(); + + for (const pkg of packages) { + if (!toBuild.has(pkg.packageJson.name)) { + continue; + } + const role = pkg.packageJson.backstage?.role; + if (!role) { + console.warn(`Ignored ${pkg.packageJson.name} because it has no role`); + customBuild.push(pkg.packageJson.name); + continue; + } + + const buildScript = pkg.packageJson.scripts?.build; + if (!buildScript) { + customBuild.push(pkg.packageJson.name); + continue; + } + + if (!buildScript.startsWith('backstage-cli script build')) { + console.warn( + `Ignored ${pkg.packageJson.name} because it has a custom build script, '${buildScript}'`, + ); + customBuild.push(pkg.packageJson.name); + continue; + } + + const outputs = getOutputsForRole(role); + + // No need to build and include types in the production runtime + outputs.delete(Output.types); + + if (outputs.size > 0) { + standardBuilds.push({ + targetDir: pkg.dir, + outputs: outputs, + logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `, + // No need to detect these for the backend builds, we assume no minification or types + minify: false, + useApiExtractor: false, + }); + } + } + + await buildPackages(standardBuilds); + + if (customBuild.length > 0) { + const scopeArgs = customBuild.flatMap(name => ['--scope', name]); + const lernaArgs = + options.parallelism && Number.isInteger(options.parallelism) + ? ['--concurrency', options.parallelism.toString()] + : []; + + await run('yarn', ['lerna', ...lernaArgs, 'run', ...scopeArgs, 'build'], { + cwd: paths.targetRoot, + }); + } + } + + await moveToDistWorkspace(targetDir, targets); + + const files: FileEntry[] = options.files ?? ['yarn.lock', 'package.json']; + + for (const file of files) { + const src = typeof file === 'string' ? file : file.src; + const dest = typeof file === 'string' ? file : file.dest; + await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest)); + } + + if (options.skeleton) { + const skeletonFiles = targets.map(target => { + const dir = relativePath(paths.targetRoot, target.dir); + return joinPath(dir, 'package.json'); + }); + + await tar.create( + { + file: resolvePath(targetDir, options.skeleton), + cwd: targetDir, + portable: true, + noMtime: true, + gzip: options.skeleton.endsWith('.gz'), + } as CreateOptions & { noMtime: boolean }, + skeletonFiles, + ); + } + + return targetDir; +} + +const FAST_PACK_SCRIPTS = [ + undefined, + 'backstage-cli prepack', + 'backstage-cli script prepack', +]; + +async function moveToDistWorkspace( + workspaceDir: string, + localPackages: PackageGraphNode[], +): Promise { + const [fastPackPackages, slowPackPackages] = partition(localPackages, pkg => + FAST_PACK_SCRIPTS.includes(pkg.packageJson.scripts?.prepack), + ); + + // New an improved flow where we avoid calling `yarn pack` + await Promise.all( + fastPackPackages.map(async target => { + console.log(`Moving ${target.name} into dist workspace`); + + const outputDir = relativePath(paths.targetRoot, target.dir); + const absoluteOutputPath = resolvePath(workspaceDir, outputDir); + await copyPackageDist(target.dir, absoluteOutputPath); + }), + ); + + // Old flow is below, which calls `yarn pack` and extracts the tarball + + async function pack(target: PackageGraphNode, archive: string) { + console.log(`Repacking ${target.name} into dist workspace`); + const archivePath = resolvePath(workspaceDir, archive); + + await run('yarn', ['pack', '--filename', archivePath], { + cwd: target.dir, + }); + // TODO(Rugvip): yarn pack doesn't call postpack, once the bug is fixed this can be removed + if (target.packageJson?.scripts?.postpack) { + await run('yarn', ['postpack'], { cwd: target.dir }); + } + + const outputDir = relativePath(paths.targetRoot, target.dir); + const absoluteOutputPath = resolvePath(workspaceDir, outputDir); + await fs.ensureDir(absoluteOutputPath); + + await tar.extract({ + file: archivePath, + cwd: absoluteOutputPath, + strip: 1, + }); + await fs.remove(archivePath); + + // We remove the dependencies from package.json of packages that are marked + // as bundled, so that yarn doesn't try to install them. + if (target.packageJson.bundled) { + const pkgJson = await fs.readJson( + resolvePath(absoluteOutputPath, 'package.json'), + ); + delete pkgJson.dependencies; + delete pkgJson.devDependencies; + delete pkgJson.peerDependencies; + delete pkgJson.optionalDependencies; + + await fs.writeJson( + resolvePath(absoluteOutputPath, 'package.json'), + pkgJson, + { + spaces: 2, + }, + ); + } + } + + const [unsafePackages, safePackages] = partition(slowPackPackages, p => + UNSAFE_PACKAGES.includes(p.name), + ); + + // The unsafe package are packed first one by one in order to avoid race conditions + // where the CLI is being executed with broken dependencies. + for (const target of unsafePackages) { + await pack(target, `temp-package.tgz`); + } + + // Repacking in parallel is much faster and safe for all packages outside of the Backstage repo + await Promise.all( + safePackages.map(async (target, index) => + pack(target, `temp-package-${index}.tgz`), + ), + ); +} diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index c35e2587b9..75f3fdf71d 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2022 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,213 +14,4 @@ * limitations under the License. */ -import fs from 'fs-extra'; -import { - join as joinPath, - resolve as resolvePath, - relative as relativePath, -} from 'path'; -import { tmpdir } from 'os'; -import tar, { CreateOptions } from 'tar'; -import { paths } from '../paths'; -import { run } from '../run'; -import { ParallelOption } from '../parallel'; -import { - dependencies as cliDependencies, - devDependencies as cliDevDependencies, -} from '../../../package.json'; -import { getPackages } from '@manypkg/get-packages'; -import { PackageGraph, PackageGraphNode } from '../monorepo'; - -// These packages aren't safe to pack in parallel since the CLI depends on them -const UNSAFE_PACKAGES = [ - ...Object.keys(cliDependencies), - ...Object.keys(cliDevDependencies), -]; - -type FileEntry = - | string - | { - src: string; - dest: string; - }; - -type Options = { - /** - * Target directory for the dist workspace, defaults to a temporary directory - */ - targetDir?: string; - - /** - * Files to copy into the target workspace. - * - * Defaults to ['yarn.lock', 'package.json']. - */ - files?: FileEntry[]; - - /** - * If set to true, the target packages are built before they are packaged into the workspace. - */ - buildDependencies?: boolean; - - /** - * When `buildDependencies` is set, this list of packages will not be built even if they are dependencies. - */ - buildExcludes?: string[]; - - /** - * Enable (true/false) or control amount of (number) parallelism in some build steps. - */ - parallel?: ParallelOption; - - /** - * If set, creates a skeleton tarball that contains all package.json files - * with the same structure as the workspace dir. - */ - skeleton?: 'skeleton.tar' | 'skeleton.tar.gz'; -}; - -/** - * Uses `yarn pack` to package local packages and unpacks them into a dist workspace. - * The target workspace will end up containing dist version of each package and - * will be suitable for packaging e.g. into a docker image. - * - * This creates a structure that is functionally similar to if the packages where - * installed from npm, but uses Yarn workspaces to link to them at runtime. - */ -export async function createDistWorkspace( - packageNames: string[], - options: Options = {}, -) { - const targetDir = - options.targetDir ?? - (await fs.mkdtemp(resolvePath(tmpdir(), 'dist-workspace'))); - - const { packages } = await getPackages(paths.targetDir); - const packageGraph = PackageGraph.fromPackages(packages); - const targetNames = packageGraph.collectPackageNames(packageNames, node => { - // Don't include dependencies of packages that are marked as bundled - if (node.packageJson.bundled) { - return undefined; - } - - return node.publishedLocalDependencies.keys(); - }); - const targets = Array.from(targetNames).map(name => packageGraph.get(name)!); - - if (options.buildDependencies) { - const exclude = options.buildExcludes ?? []; - - const toBuild = targets.filter(target => !exclude.includes(target.name)); - if (toBuild.length > 0) { - const scopeArgs = toBuild.flatMap(target => ['--scope', target.name]); - const lernaArgs = - options.parallel && Number.isInteger(options.parallel) - ? ['--concurrency', options.parallel.toString()] - : []; - - await run('yarn', ['lerna', ...lernaArgs, 'run', ...scopeArgs, 'build'], { - cwd: paths.targetRoot, - }); - } - } - - await moveToDistWorkspace(targetDir, targets); - - const files: FileEntry[] = options.files ?? ['yarn.lock', 'package.json']; - - for (const file of files) { - const src = typeof file === 'string' ? file : file.src; - const dest = typeof file === 'string' ? file : file.dest; - await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest)); - } - - if (options.skeleton) { - const skeletonFiles = targets.map(target => { - const dir = relativePath(paths.targetRoot, target.dir); - return joinPath(dir, 'package.json'); - }); - - await tar.create( - { - file: resolvePath(targetDir, options.skeleton), - cwd: targetDir, - portable: true, - noMtime: true, - gzip: options.skeleton.endsWith('.gz'), - } as CreateOptions & { noMtime: boolean }, - skeletonFiles, - ); - } - - return targetDir; -} - -async function moveToDistWorkspace( - workspaceDir: string, - localPackages: PackageGraphNode[], -): Promise { - async function pack(target: PackageGraphNode, archive: string) { - console.log(`Repacking ${target.name} into dist workspace`); - const archivePath = resolvePath(workspaceDir, archive); - - await run('yarn', ['pack', '--filename', archivePath], { - cwd: target.dir, - }); - // TODO(Rugvip): yarn pack doesn't call postpack, once the bug is fixed this can be removed - if (target.packageJson?.scripts?.postpack) { - await run('yarn', ['postpack'], { cwd: target.dir }); - } - - const outputDir = relativePath(paths.targetRoot, target.dir); - const absoluteOutputPath = resolvePath(workspaceDir, outputDir); - await fs.ensureDir(absoluteOutputPath); - - await tar.extract({ - file: archivePath, - cwd: absoluteOutputPath, - strip: 1, - }); - await fs.remove(archivePath); - - // We remove the dependencies from package.json of packages that are marked - // as bundled, so that yarn doesn't try to install them. - if (target.packageJson.bundled) { - const pkgJson = await fs.readJson( - resolvePath(absoluteOutputPath, 'package.json'), - ); - delete pkgJson.dependencies; - delete pkgJson.devDependencies; - delete pkgJson.peerDependencies; - delete pkgJson.optionalDependencies; - - await fs.writeJson( - resolvePath(absoluteOutputPath, 'package.json'), - pkgJson, - { - spaces: 2, - }, - ); - } - } - - const unsafePackages = localPackages.filter(p => - UNSAFE_PACKAGES.includes(p.name), - ); - const safePackages = localPackages.filter( - p => !UNSAFE_PACKAGES.includes(p.name), - ); - - // The unsafe package are packed first one by one in order to avoid race conditions - // where the CLI is being executed with broken dependencies. - for (const target of unsafePackages) { - await pack(target, `temp-package.tgz`); - } - - // Repacking in parallel is much faster and safe for all packages outside of the Backstage repo - await Promise.all( - safePackages.map(async (target, index) => - pack(target, `temp-package-${index}.tgz`), - ), - ); -} +export { createDistWorkspace } from './createDistWorkspace'; diff --git a/packages/cli/src/lib/parallel.test.ts b/packages/cli/src/lib/parallel.test.ts index c5c33473ff..4fbcae2191 100644 --- a/packages/cli/src/lib/parallel.test.ts +++ b/packages/cli/src/lib/parallel.test.ts @@ -14,46 +14,198 @@ * limitations under the License. */ -import { isParallelDefault, parseParallel } from './parallel'; +import { + parseParallelismOption, + getEnvironmentParallelism, + runParallelWorkers, + runWorkerQueueThreads, + runWorkerThreads, +} from './parallel'; -describe('parallel', () => { - describe('parseParallel', () => { - it('coerces "false" string to boolean', () => { - expect(parseParallel('false')).toBeFalsy(); - }); - - it('coerces "true" to boolean', () => { - expect(parseParallel('true')).toBeTruthy(); - }); - - it('coerces number string to number', () => { - expect(parseParallel('2')).toBe(2); - }); - it.each([[true], [false], [2]])('returns itself for %p', value => { - expect(parseParallel(value as any)).toEqual(value); - }); - - it.each([[undefined], [null]])('returns true for %p', value => { - expect(parseParallel(value as any)).toBe(true); - }); - - it.each([['on'], [2.5], ['2.5']])('throws error for %p', value => { - expect(() => parseParallel(value as any)).toThrowError( - `Parallel option value '${value}' is not a boolean or integer`, - ); - }); +describe('parseParallelismOption', () => { + it('coerces false no parallelism', () => { + expect(parseParallelismOption(false)).toBe(1); + expect(parseParallelismOption('false')).toBe(1); }); - describe('isParallelDefault', () => { - it('returns true if default value', () => { - expect(isParallelDefault(undefined)).toBeTruthy(); - expect(isParallelDefault(true)).toBeTruthy(); - }); + it('coerces true or undefined to default parallelism', () => { + expect(parseParallelismOption(true)).toBe(4); + expect(parseParallelismOption('true')).toBe(4); + expect(parseParallelismOption(undefined)).toBe(4); + expect(parseParallelismOption(null)).toBe(4); + }); - it('returns false if not default value', () => { - expect(isParallelDefault(false)).toBeFalsy(); - expect(isParallelDefault(2)).toBeFalsy(); - expect(isParallelDefault('true' as any)).toBeFalsy(); - }); + it('coerces number string to number', () => { + expect(parseParallelismOption('2')).toBe(2); + }); + + it.each([['on'], [2.5], ['2.5']])('throws error for %p', value => { + expect(() => parseParallelismOption(value as any)).toThrowError( + `Parallel option value '${value}' is not a boolean or integer`, + ); + }); +}); + +describe('getEnvironmentParallelism', () => { + it('reads the parallelism setting from the environment', () => { + process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '2'; + expect(getEnvironmentParallelism()).toBe(2); + + process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'true'; + expect(getEnvironmentParallelism()).toBe(4); + + process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'false'; + expect(getEnvironmentParallelism()).toBe(1); + + delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL; + expect(getEnvironmentParallelism()).toBe(4); + }); +}); + +describe('runParallelWorkers', () => { + it('executes work in parallel', async () => { + const started = new Array(); + const done = new Array(); + const waiting = new Array<() => void>(); + + const work = runParallelWorkers({ + items: [0, 1, 2, 3, 4], + parallelismFactor: 0.5, // 2 at a time + worker: async item => { + started.push(item); + await new Promise(resolve => { + waiting[item] = resolve; + }); + done.push(item); + }, + }); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1]); + expect(done).toEqual([]); + waiting[0](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1, 2]); + expect(done).toEqual([0]); + waiting[1](); + waiting[2](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1, 2, 3, 4]); + expect(done).toEqual([0, 1, 2]); + waiting[3](); + waiting[4](); + + await work; + expect(done).toEqual([0, 1, 2, 3, 4]); + }); + + it('executes work sequentially', async () => { + const started = new Array(); + const done = new Array(); + const waiting = new Array<() => void>(); + + const work = runParallelWorkers({ + items: [0, 1, 2, 3, 4], + parallelismFactor: 0, // 1 at a time + worker: async item => { + started.push(item); + await new Promise(resolve => { + waiting[item] = resolve; + }); + done.push(item); + }, + }); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0]); + expect(done).toEqual([]); + waiting[0](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1]); + expect(done).toEqual([0]); + waiting[1](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1, 2]); + waiting[2](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1, 2, 3]); + waiting[3](); + + await new Promise(resolve => setTimeout(resolve)); + expect(started).toEqual([0, 1, 2, 3, 4]); + waiting[4](); + + await work; + expect(done).toEqual([0, 1, 2, 3, 4]); + }); +}); + +describe('runWorkerQueueThreads', () => { + it('should execute work in parallel', async () => { + const sharedData = new SharedArrayBuffer(10); + const sharedView = new Uint8Array(sharedData); + + const results = await runWorkerQueueThreads({ + threadCount: 4, + workerData: sharedData, + items: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + workerFactory: data => { + const view = new Uint8Array(data); + + return async (i: number) => { + view[i] = 10 + i; + return 20 + i; + }; + }, + }); + + expect(Array.from(sharedView)).toEqual([ + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + ]); + expect(results).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29]); + }); +}); + +describe('runWorkerThreads', () => { + it('should run a single thread without items', async () => { + const [result] = await runWorkerThreads({ + threadCount: 1, + workerData: 'foo', + worker: async data => `${data}bar`, + }); + + expect(result).toBe('foobar'); + }); + + it('should run multiple threads without items', async () => { + const results = await runWorkerThreads({ + threadCount: 4, + worker: async () => 'foo', + }); + + expect(results).toEqual(['foo', 'foo', 'foo', 'foo']); + }); + + it('should send messages', async () => { + const messages = new Array(); + + await runWorkerThreads({ + threadCount: 2, + worker: async (_data, sendMessage) => { + sendMessage('a'); + await new Promise(resolve => setTimeout(resolve, 10)); + sendMessage('b'); + await new Promise(resolve => setTimeout(resolve, 10)); + sendMessage('c'); + }, + onMessage: (message: string) => messages.push(message), + }); + + expect(messages.sort()).toEqual(['a', 'a', 'b', 'b', 'c', 'c']); }); }); diff --git a/packages/cli/src/lib/parallel.ts b/packages/cli/src/lib/parallel.ts index f922295a89..246020fcc3 100644 --- a/packages/cli/src/lib/parallel.ts +++ b/packages/cli/src/lib/parallel.ts @@ -14,30 +14,34 @@ * limitations under the License. */ +import { ErrorLike } from '@backstage/errors'; +import { Worker } from 'worker_threads'; + +export const DEFAULT_PARALLELISM = 4; + export const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL'; -export type ParallelOption = boolean | number | undefined; +export type ParallelismOption = boolean | string | number | null | undefined; -export function isParallelDefault(parallel: ParallelOption) { - return parallel === undefined || parallel === true; -} - -export function parseParallel( - parallel: boolean | string | number | undefined, -): ParallelOption { +export function parseParallelismOption(parallel: ParallelismOption): number { if (parallel === undefined || parallel === null) { - return true; + return DEFAULT_PARALLELISM; } else if (typeof parallel === 'boolean') { - return parallel; + return parallel ? DEFAULT_PARALLELISM : 1; } else if (typeof parallel === 'number' && Number.isInteger(parallel)) { + if (parallel < 1) { + return 1; + } return parallel; } else if (typeof parallel === 'string') { if (parallel === 'true') { - return true; + return parseParallelismOption(true); } else if (parallel === 'false') { - return false; - } else if (Number.isInteger(parseFloat(parallel.toString()))) { - return Number(parallel); + return parseParallelismOption(false); + } + const parsed = Number(parallel); + if (Number.isInteger(parsed)) { + return parseParallelismOption(parsed); } } @@ -45,3 +49,278 @@ export function parseParallel( `Parallel option value '${parallel}' is not a boolean or integer`, ); } + +export function getEnvironmentParallelism() { + return parseParallelismOption(process.env[PARALLEL_ENV_VAR]); +} + +type ParallelWorkerOptions = { + /** + * Decides the number of parallel workers by multiplying + * this with the configured parallelism, which defaults to 4. + * + * Defaults to 1. + */ + parallelismFactor?: number; + parallelismSetting?: ParallelismOption; + items: Iterable; + worker: (item: TItem) => Promise; +}; + +export async function runParallelWorkers( + options: ParallelWorkerOptions, +) { + const { parallelismFactor = 1, parallelismSetting, items, worker } = options; + const parallelism = parallelismSetting + ? parseParallelismOption(parallelismSetting) + : getEnvironmentParallelism(); + + const sharedIterator = items[Symbol.iterator](); + const sharedIterable = { + [Symbol.iterator]: () => sharedIterator, + }; + + const workerCount = Math.max(Math.floor(parallelismFactor * parallelism), 1); + return Promise.all( + Array(workerCount) + .fill(0) + .map(async () => { + for (const value of sharedIterable) { + await worker(value); + } + }), + ); +} + +type WorkerThreadMessage = + | { + type: 'done'; + } + | { + type: 'item'; + index: number; + item: unknown; + } + | { + type: 'start'; + } + | { + type: 'result'; + index: number; + result: unknown; + } + | { + type: 'error'; + error: ErrorLike; + } + | { + type: 'message'; + message: unknown; + }; + +export type WorkerQueueThreadsOptions = { + /** The items to process */ + items: Iterable; + /** + * A function that will be called within each worker thread at startup, + * which should return the worker function that will be called for each item. + * + * This function must be defined as an arrow function or using the + * function keyword, and must be entirely self contained, not referencing + * any variables outside of its scope. This is because the function source + * is stringified and evaluated in the worker thread. + * + * To pass data to the worker, use the `workerData` option and `items`, but + * note that they are both copied by value into the worker thread, except for + * types that are explicitly shareable across threads, such as `SharedArrayBuffer`. + */ + workerFactory: (data: TData) => (item: TItem) => Promise; + /** Data supplied to each worker factory */ + workerData?: TData; + /** Number of threads, defaults to the environment parallelism */ + threadCount?: number; +}; + +/** + * Spawns one or more worker threads using the `worker_threads` module. + * Each thread processes one item at a time from the provided `options.items`. + */ +export async function runWorkerQueueThreads( + options: WorkerQueueThreadsOptions, +): Promise { + const { + workerFactory, + workerData, + threadCount = getEnvironmentParallelism(), + } = options; + + const iterator = options.items[Symbol.iterator](); + const results = new Array(); + let itemIndex = 0; + + await Promise.all( + Array(threadCount) + .fill(0) + .map(async () => { + const thread = new Worker(`(${workerQueueThread})(${workerFactory})`, { + eval: true, + workerData, + }); + + return new Promise((resolve, reject) => { + thread.on('message', (message: WorkerThreadMessage) => { + if (message.type === 'start' || message.type === 'result') { + if (message.type === 'result') { + results[message.index] = message.result as TResult; + } + const { value, done } = iterator.next(); + if (done) { + thread.postMessage({ type: 'done' }); + } else { + thread.postMessage({ + type: 'item', + index: itemIndex, + item: value, + }); + itemIndex += 1; + } + } else if (message.type === 'error') { + const error = new Error(message.error.message); + error.name = message.error.name; + error.stack = message.error.stack; + reject(error); + } + }); + + thread.on('error', reject); + thread.on('exit', (code: number) => { + if (code !== 0) { + reject(new Error(`Worker thread exited with code ${code}`)); + } else { + resolve(); + } + }); + }); + }), + ); + + return results; +} + +function workerQueueThread( + workerFuncFactory: (data: unknown) => (item: unknown) => Promise, +) { + const { parentPort, workerData } = require('worker_threads'); + const workerFunc = workerFuncFactory(workerData); + + parentPort.on('message', async (message: WorkerThreadMessage) => { + if (message.type === 'done') { + parentPort.close(); + return; + } + if (message.type === 'item') { + try { + const result = await workerFunc(message.item); + parentPort.postMessage({ + type: 'result', + index: message.index, + result, + }); + } catch (error) { + parentPort.postMessage({ type: 'error', error }); + } + } + }); + + parentPort.postMessage({ type: 'start' }); +} + +export type WorkerThreadsOptions = { + /** + * A function that is called by each worker thread to produce a result. + * + * This function must be defined as an arrow function or using the + * function keyword, and must be entirely self contained, not referencing + * any variables outside of its scope. This is because the function source + * is stringified and evaluated in the worker thread. + * + * To pass data to the worker, use the `workerData` option, but + * note that they are both copied by value into the worker thread, except for + * types that are explicitly shareable across threads, such as `SharedArrayBuffer`. + */ + worker: ( + data: TData, + sendMessage: (message: TMessage) => void, + ) => Promise; + /** Data supplied to each worker */ + workerData?: TData; + /** Number of threads, defaults to 1 */ + threadCount?: number; + /** An optional handler for messages posted from the worker thread */ + onMessage?: (message: TMessage) => void; +}; + +/** + * Spawns one or more worker threads using the `worker_threads` module. + */ +export async function runWorkerThreads( + options: WorkerThreadsOptions, +): Promise { + const { worker, workerData, threadCount = 1, onMessage } = options; + + return Promise.all( + Array(threadCount) + .fill(0) + .map(async () => { + const thread = new Worker(`(${workerThread})(${worker})`, { + eval: true, + workerData, + }); + + return new Promise((resolve, reject) => { + thread.on('message', (message: WorkerThreadMessage) => { + if (message.type === 'result') { + resolve(message.result as TResult); + } else if (message.type === 'error') { + reject(message.error); + } else if (message.type === 'message') { + onMessage?.(message.message as TMessage); + } + }); + + thread.on('error', reject); + thread.on('exit', (code: number) => { + reject( + new Error(`Unexpected worker thread exit with code ${code}`), + ); + }); + }); + }), + ); +} + +function workerThread( + workerFunc: ( + data: unknown, + sendMessage: (message: unknown) => void, + ) => Promise, +) { + const { parentPort, workerData } = require('worker_threads'); + + const sendMessage = (message: unknown) => { + parentPort.postMessage({ type: 'message', message }); + }; + + workerFunc(workerData, sendMessage).then( + result => { + parentPort.postMessage({ + type: 'result', + index: 0, + result, + }); + }, + error => { + parentPort.postMessage({ type: 'error', error }); + }, + ); +} diff --git a/packages/cli/src/lib/role/index.ts b/packages/cli/src/lib/role/index.ts new file mode 100644 index 0000000000..4e1047a628 --- /dev/null +++ b/packages/cli/src/lib/role/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { + PackageRoleInfo, + PackagePlatform, + PackageOutputType, + PackageRole, +} from './types'; +export { + getRoleInfo, + getRoleFromPackage, + findRoleFromCommand, + detectRoleFromPackage, +} from './packageRoles'; diff --git a/packages/cli/src/lib/role/packageRoles.test.ts b/packages/cli/src/lib/role/packageRoles.test.ts new file mode 100644 index 0000000000..f6c9fb418a --- /dev/null +++ b/packages/cli/src/lib/role/packageRoles.test.ts @@ -0,0 +1,328 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import mockFs from 'mock-fs'; +import { Command } from 'commander'; +import { + getRoleInfo, + getRoleFromPackage, + findRoleFromCommand, + detectRoleFromPackage, +} from './packageRoles'; + +describe('getRoleInfo', () => { + it('provides role info by role', () => { + expect(getRoleInfo('web-library')).toEqual({ + role: 'web-library', + platform: 'web', + output: ['types', 'esm'], + }); + + expect(getRoleInfo('app')).toEqual({ + role: 'app', + platform: 'web', + output: ['bundle'], + }); + + expect(() => getRoleInfo('invalid')).toThrow( + `Unknown package role 'invalid'`, + ); + }); +}); + +describe('getRoleFromPackage', () => { + it('reads explicit package roles', () => { + expect( + getRoleFromPackage({ + backstage: { + role: 'web-library', + }, + }), + ).toEqual('web-library'); + + expect( + getRoleFromPackage({ + backstage: { + role: 'app', + }, + }), + ).toEqual('app'); + + expect(() => + getRoleFromPackage({ + name: 'test', + backstage: {}, + }), + ).toThrow('Package test must specify a role in the "backstage" field'); + + expect(() => + getRoleFromPackage({ + name: 'test', + backstage: { role: 'invalid' }, + }), + ).toThrow(`Unknown package role 'invalid'`); + }); +}); + +describe('findRoleFromCommand', () => { + function mkCommand(args: string) { + return new Command() + .option('--role ', 'test role') + .parse(['node', 'entry.js', ...args.split(' ')]) as Command; + } + + beforeEach(() => { + mockFs({ + 'package.json': JSON.stringify({ + name: 'test', + backstage: { + role: 'web-library', + }, + }), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('provides role info by role', async () => { + await expect(findRoleFromCommand(mkCommand(''))).resolves.toEqual( + 'web-library', + ); + + await expect( + findRoleFromCommand(mkCommand('--role node-library')), + ).resolves.toEqual('node-library'); + + await expect( + findRoleFromCommand(mkCommand('--role invalid')), + ).rejects.toThrow(`Unknown package role 'invalid'`); + }); +}); + +describe('detectRoleFromPackage', () => { + it('detects the role of example-app', () => { + expect( + detectRoleFromPackage({ + name: 'example-app', + private: true, + bundled: true, + scripts: { + start: 'backstage-cli app:serve', + build: 'backstage-cli app:build', + clean: 'backstage-cli clean', + test: 'backstage-cli test', + 'test:e2e': + 'start-server-and-test start http://localhost:3000 cy:dev', + 'test:e2e:ci': + 'start-server-and-test start http://localhost:3000 cy:run', + lint: 'backstage-cli lint', + 'cy:dev': 'cypress open', + 'cy:run': 'cypress run', + }, + }), + ).toEqual('app'); + }); + + it('detects the role of example-backend', () => { + expect( + detectRoleFromPackage({ + name: 'example-backend', + main: 'dist/index.cjs.js', + types: 'src/index.ts', + scripts: { + build: 'backstage-cli backend:bundle', + 'build-image': + 'docker build ../.. -f Dockerfile --tag example-backend', + start: 'backstage-cli backend:dev', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + clean: 'backstage-cli clean', + 'migrate:create': 'knex migrate:make -x ts', + }, + }), + ).toEqual('backend'); + }); + + it('detects the role of @backstage/plugin-catalog', () => { + expect( + detectRoleFromPackage({ + name: '@backstage/plugin-catalog', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.esm.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli plugin:build', + start: 'backstage-cli plugin:serve', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + diff: 'backstage-cli plugin:diff', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual('plugin-frontend'); + }); + + it('detects the role of @backstage/plugin-catalog-backend', () => { + expect( + detectRoleFromPackage({ + name: '@backstage/plugin-catalog-backend', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + types: 'dist/index.d.ts', + }, + scripts: { + start: 'backstage-cli backend:dev', + build: 'backstage-cli backend:build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual('plugin-backend'); + }); + + it('detects the role of @backstage/plugin-catalog-react', () => { + expect( + detectRoleFromPackage({ + name: '@backstage/plugin-catalog-react', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.esm.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual('web-library'); + }); + + it('detects the role of @backstage/plugin-catalog-common', () => { + expect( + detectRoleFromPackage({ + name: '@backstage/plugin-catalog-common', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + module: 'dist/index.esm.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli build', + lint: 'backstage-cli lint', + test: 'backstage-cli test --passWithNoTests', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual('common-library'); + }); + + it('detects the role of @backstage/plugin-catalog-backend-module-ldap', () => { + expect( + detectRoleFromPackage({ + name: '@backstage/plugin-catalog-backend-module-ldap', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli backend:build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual('plugin-backend-module'); + }); + + it('detects the role of @backstage/plugin-permission-node', () => { + expect( + detectRoleFromPackage({ + name: '@backstage/plugin-permission-node', + main: 'src/index.ts', + types: 'src/index.ts', + homepage: 'https://backstage.io', + publishConfig: { + access: 'public', + main: 'dist/index.cjs.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli backend:build', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual('node-library'); + }); + + it('detects the role of @backstage/plugin-analytics-module-ga', () => { + expect( + detectRoleFromPackage({ + name: '@backstage/plugin-analytics-module-ga', + main: 'src/index.ts', + types: 'src/index.ts', + publishConfig: { + access: 'public', + main: 'dist/index.esm.js', + types: 'dist/index.d.ts', + }, + scripts: { + build: 'backstage-cli plugin:build', + start: 'backstage-cli plugin:serve', + lint: 'backstage-cli lint', + test: 'backstage-cli test', + diff: 'backstage-cli plugin:diff', + prepack: 'backstage-cli prepack', + postpack: 'backstage-cli postpack', + clean: 'backstage-cli clean', + }, + }), + ).toEqual('plugin-frontend-module'); + }); +}); diff --git a/packages/cli/src/lib/role/packageRoles.ts b/packages/cli/src/lib/role/packageRoles.ts new file mode 100644 index 0000000000..7c855ee0fd --- /dev/null +++ b/packages/cli/src/lib/role/packageRoles.ts @@ -0,0 +1,186 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { z } from 'zod'; +import fs from 'fs-extra'; +import { Command } from 'commander'; +import { paths } from '../paths'; +import { PackageRole, PackageRoleInfo } from './types'; + +const packageRoleInfos: PackageRoleInfo[] = [ + { + role: 'app', + platform: 'web', + output: ['bundle'], + }, + { + role: 'backend', + platform: 'node', + output: ['bundle'], + }, + { + role: 'cli', + platform: 'node', + output: ['cjs'], + }, + { + role: 'web-library', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'node-library', + platform: 'node', + output: ['types', 'cjs'], + }, + { + role: 'common-library', + platform: 'common', + output: ['types', 'esm', 'cjs'], + }, + { + role: 'plugin-frontend', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'plugin-frontend-module', + platform: 'web', + output: ['types', 'esm'], + }, + { + role: 'plugin-backend', + platform: 'node', + output: ['types', 'cjs'], + }, + { + role: 'plugin-backend-module', + platform: 'node', + output: ['types', 'cjs'], + }, +]; + +export function getRoleInfo(role: string): PackageRoleInfo { + const roleInfo = packageRoleInfos.find(r => r.role === role); + if (!roleInfo) { + throw new Error(`Unknown package role '${role}'`); + } + return roleInfo; +} + +const readSchema = z.object({ + name: z.string().optional(), + backstage: z + .object({ + role: z.string().optional(), + }) + .optional(), +}); + +export function getRoleFromPackage(pkgJson: unknown): PackageRole | undefined { + const pkg = readSchema.parse(pkgJson); + + // If there's an explicit role, use that. + if (pkg.backstage) { + const { role } = pkg.backstage; + if (!role) { + throw new Error( + `Package ${pkg.name} must specify a role in the "backstage" field`, + ); + } + + return getRoleInfo(role).role; + } + + return undefined; +} + +export async function findRoleFromCommand(cmd: Command): Promise { + if (cmd.role) { + return getRoleInfo(cmd.role)?.role; + } + + const pkg = await fs.readJson(paths.resolveTarget('package.json')); + const info = getRoleFromPackage(pkg); + if (!info) { + throw new Error(`Target package must have 'backstage.role' set`); + } + return info; +} + +const detectionSchema = z.object({ + name: z.string().optional(), + scripts: z + .object({ + start: z.string().optional(), + build: z.string().optional(), + }) + .optional(), + publishConfig: z + .object({ + main: z.string().optional(), + types: z.string().optional(), + module: z.string().optional(), + }) + .optional(), + main: z.string().optional(), + types: z.string().optional(), + module: z.string().optional(), +}); + +export function detectRoleFromPackage( + pkgJson: unknown, +): PackageRole | undefined { + const pkg = detectionSchema.parse(pkgJson); + + if (pkg.scripts?.start?.includes('app:serve')) { + return 'app'; + } + if (pkg.scripts?.build?.includes('backend:bundle')) { + return 'backend'; + } + if (pkg.name?.includes('plugin-') && pkg.name?.includes('-backend-module-')) { + return 'plugin-backend-module'; + } + if (pkg.name?.includes('plugin-') && pkg.name?.includes('-module-')) { + return 'plugin-frontend-module'; + } + if (pkg.scripts?.start?.includes('plugin:serve')) { + return 'plugin-frontend'; + } + if (pkg.scripts?.start?.includes('backend:dev')) { + return 'plugin-backend'; + } + + const mainEntry = pkg.publishConfig?.main || pkg.main; + const moduleEntry = pkg.publishConfig?.module || pkg.module; + const typesEntry = pkg.publishConfig?.types || pkg.types; + if (typesEntry) { + if (mainEntry && moduleEntry) { + return 'common-library'; + } + if (moduleEntry || mainEntry?.endsWith('.esm.js')) { + return 'web-library'; + } + if (mainEntry) { + return 'node-library'; + } + } else if (mainEntry) { + return 'cli'; + } + + return undefined; +} diff --git a/packages/cli/src/lib/role/types.ts b/packages/cli/src/lib/role/types.ts new file mode 100644 index 0000000000..6a89f7bdaf --- /dev/null +++ b/packages/cli/src/lib/role/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type PackageRole = + | 'app' + | 'backend' + | 'cli' + | 'web-library' + | 'node-library' + | 'common-library' + | 'plugin-frontend' + | 'plugin-frontend-module' + | 'plugin-backend' + | 'plugin-backend-module'; + +export type PackagePlatform = 'node' | 'web' | 'common'; +export type PackageOutputType = 'bundle' | 'types' | 'esm' | 'cjs'; + +export interface PackageRoleInfo { + role: PackageRole; + platform: PackagePlatform; + output: PackageOutputType[]; +} diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/lib/versioning/packages.ts index 1dabec6032..ed7266a692 100644 --- a/packages/cli/src/lib/versioning/packages.ts +++ b/packages/cli/src/lib/versioning/packages.ts @@ -28,7 +28,7 @@ const DEP_TYPES = [ // Package data as returned by `yarn info` export type YarnInfoInspectData = { name: string; - 'dist-tags': { latest: string }; + 'dist-tags': Record; versions: string[]; time: { [version: string]: string }; }; @@ -67,7 +67,10 @@ export async function mapDependencies( targetDir: string, pattern: string, ): Promise> { - const { packages } = await getPackages(targetDir); + const { packages, root } = await getPackages(targetDir); + + // Include root package.json too + packages.push(root); const dependencyMap = new Map(); for (const pkg of packages) { diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 37bcd94186..321ca9c546 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -31,7 +31,7 @@ "express": "{{versionQuery 'express' '4.17.1'}}", "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", "winston": "{{versionQuery 'winston' '3.2.1'}}", - "cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}", + "node-fetch": "{{versionQuery 'node-fetch' '2.6.7'}}", "yn": "{{versionQuery 'yn' '4.0.0'}}" }, "devDependencies": { diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 7461b0a57b..d38a31fa7d 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -48,7 +48,7 @@ "@types/jest": "{{versionQuery '@types/jest' '26.0.7'}}", "@types/node": "{{versionQuery '@types/node' '14.14.32'}}", "msw": "{{versionQuery 'msw' '0.35.0'}}", - "cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}" + "cross-fetch": "{{versionQuery 'cross-fetch' '3.1.5'}}" }, "files": [ "dist" diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 8f5b1bcbbc..c7283f5d8c 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/codemods +## 0.1.33 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/core-app-api@0.5.2 + +## 0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + - @backstage/core-app-api@0.5.2-next.0 + +## 0.1.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.1.31 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 98ce532aee..dc1fe6a537 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.31", + "version": "0.1.33", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index bb59800a99..b374a02fbf 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -41,7 +41,7 @@ "json-schema": "^0.4.0", "json-schema-merge-allof": "^0.8.1", "json-schema-traverse": "^1.0.0", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "typescript-json-schema": "^0.52.0", "yaml": "^1.9.2", "yup": "^0.32.9" diff --git a/packages/config/package.json b/packages/config/package.json index 44fb7ab286..02c63af2b5 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -34,7 +34,7 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/test-utils": "^0.2.3", + "@backstage/test-utils": "^0.2.4-next.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 06692a0b46..cc115a7da2 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/core-app-api +## 0.5.2 + +### Patch Changes + +- 40775bd263: Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`. + + > **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change. + +## 0.5.2-next.0 + +### Patch Changes + +- 40775bd263: Switched out the `GithubAuth` implementation to use the common `OAuth2` implementation. This relies on the simultaneous change in `@backstage/plugin-auth-backend` that enabled access token storage in cookies rather than the current solution that's based on `LocalStorage`. + + > **NOTE:** Make sure you upgrade the `auth-backend` deployment before or at the same time as you deploy this change. + ## 0.5.1 ### Patch Changes diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index ba2ed8cb62..3b53813615 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -35,6 +35,7 @@ import { FeatureFlag } from '@backstage/core-plugin-api'; import { FeatureFlagsApi } from '@backstage/core-plugin-api'; import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api'; import { FetchApi } from '@backstage/core-plugin-api'; +import { githubAuthApiRef } from '@backstage/core-plugin-api'; import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; @@ -379,25 +380,11 @@ export type FlatRoutesProps = { }; // @public -export class GithubAuth implements OAuthApi, SessionApi { - // (undocumented) - static create(options: OAuthApiCreateOptions): GithubAuth; - // (undocumented) - getAccessToken(scope?: string, options?: AuthRequestOptions): Promise; - // (undocumented) - getBackstageIdentity( - options?: AuthRequestOptions, - ): Promise; - // (undocumented) - getProfile(options?: AuthRequestOptions): Promise; +export class GithubAuth { // (undocumented) + static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T; + // @deprecated (undocumented) static normalizeScope(scope?: string): Set; - // (undocumented) - sessionState$(): Observable; - // (undocumented) - signIn(): Promise; - // (undocumented) - signOut(): Promise; } // @public @deprecated diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index e6bf5e9582..04e87b5d0c 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.5.1", + "version": "0.5.2", "private": false, "publishConfig": { "access": "public", @@ -45,8 +45,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2-next.0", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -54,7 +54,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/zen-observable": "^0.8.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts index 8bcd4cb7a5..ea7b4ac688 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts @@ -14,16 +14,33 @@ * limitations under the License. */ +import { UrlPatternDiscovery } from '../../DiscoveryApi'; +import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import GithubAuth from './GithubAuth'; -describe('GithubAuth', () => { - it('should get access token', async () => { - const getSession = jest - .fn() - .mockResolvedValue({ providerInfo: { accessToken: 'access-token' } }); - const githubAuth = new (GithubAuth as any)({ getSession }) as GithubAuth; +const getSession = jest.fn(); - expect(await githubAuth.getAccessToken()).toBe('access-token'); - expect(getSession).toBeCalledTimes(1); +jest.mock('../../../../lib/AuthSessionManager', () => ({ + ...(jest.requireActual('../../../../lib/AuthSessionManager') as any), + RefreshingAuthSessionManager: class { + getSession = getSession; + }, +})); + +describe('GithubAuth', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should forward access token request to session manager', async () => { + const githubAuth = GithubAuth.create({ + oauthRequestApi: new MockOAuthApi(), + discoveryApi: UrlPatternDiscovery.compile('http://example.com'), + }); + + githubAuth.getAccessToken('repo'); + expect(getSession).toHaveBeenCalledWith({ + scopes: new Set(['repo']), + }); }); }); diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts index dc2c15bc16..b0af4c7ade 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -14,35 +14,9 @@ * limitations under the License. */ -import { - AuthRequestOptions, - BackstageIdentityResponse, - OAuthApi, - ProfileInfo, - SessionApi, - SessionState, -} from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/types'; -import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; -import { - AuthSessionStore, - RefreshingAuthSessionManager, - StaticAuthSessionManager, -} from '../../../../lib/AuthSessionManager'; -import { OptionalRefreshSessionManagerMux } from '../../../../lib/AuthSessionManager/OptionalRefreshSessionManagerMux'; -import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { OAuth2 } from '../oauth2'; import { OAuthApiCreateOptions } from '../types'; -import { GithubSession, githubSessionSchema } from './types'; - -export type GithubAuthResponse = { - providerInfo: { - accessToken: string; - scope: string; - expiresInSeconds?: number; - }; - profile: ProfileInfo; - backstageIdentity: BackstageIdentityResponse; -}; const DEFAULT_PROVIDER = { id: 'github', @@ -55,8 +29,8 @@ const DEFAULT_PROVIDER = { * * @public */ -export default class GithubAuth implements OAuthApi, SessionApi { - static create(options: OAuthApiCreateOptions) { +export default class GithubAuth { + static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T { const { discoveryApi, environment = 'development', @@ -65,96 +39,18 @@ export default class GithubAuth implements OAuthApi, SessionApi { defaultScopes = ['read:user'], } = options; - const connector = new DefaultAuthConnector({ + return OAuth2.create({ discoveryApi, - environment, + oauthRequestApi, provider, - oauthRequestApi: oauthRequestApi, - sessionTransform(res: GithubAuthResponse): GithubSession { - return { - ...res, - providerInfo: { - accessToken: res.providerInfo.accessToken, - scopes: GithubAuth.normalizeScope(res.providerInfo.scope), - expiresAt: res.providerInfo.expiresInSeconds - ? new Date(Date.now() + res.providerInfo.expiresInSeconds * 1000) - : undefined, - }, - }; - }, + environment, + defaultScopes, }); - - const refreshingSessionManager = new RefreshingAuthSessionManager({ - connector, - defaultScopes: new Set(defaultScopes), - sessionScopes: (session: GithubSession) => session.providerInfo.scopes, - sessionShouldRefresh: (session: GithubSession) => { - const { expiresAt } = session.providerInfo; - if (!expiresAt) { - return false; - } - const expiresInSec = (expiresAt.getTime() - Date.now()) / 1000; - return expiresInSec < 60 * 5; - }, - }); - - const staticSessionManager = new AuthSessionStore({ - manager: new StaticAuthSessionManager({ - connector, - defaultScopes: new Set(defaultScopes), - sessionScopes: (session: GithubSession) => session.providerInfo.scopes, - }), - storageKey: `${provider.id}Session`, - schema: githubSessionSchema, - sessionScopes: (session: GithubSession) => session.providerInfo.scopes, - }); - - const sessionManagerMux = new OptionalRefreshSessionManagerMux({ - refreshingSessionManager, - staticSessionManager, - sessionCanRefresh: session => - session.providerInfo.expiresAt !== undefined, - }); - - return new GithubAuth(sessionManagerMux); - } - - private constructor( - private readonly sessionManager: SessionManager, - ) {} - - async signIn() { - await this.getAccessToken(); - } - - async signOut() { - await this.sessionManager.removeSession(); - } - - sessionState$(): Observable { - return this.sessionManager.sessionState$(); - } - - async getAccessToken(scope?: string, options?: AuthRequestOptions) { - const session = await this.sessionManager.getSession({ - ...options, - scopes: GithubAuth.normalizeScope(scope), - }); - return session?.providerInfo.accessToken ?? ''; - } - - async getBackstageIdentity( - options: AuthRequestOptions = {}, - ): Promise { - const session = await this.sessionManager.getSession(options); - return session?.backstageIdentity; - } - - async getProfile(options: AuthRequestOptions = {}) { - const session = await this.sessionManager.getSession(options); - return session?.profile; } + /** + * @deprecated This method is deprecated and will be removed in a future release. + */ static normalizeScope(scope?: string): Set { if (!scope) { return new Set(); diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index abbcab5e49..fe51935ab0 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/core-components +## 0.8.8 + +### Patch Changes + +- 8d785a0b1b: chore: bump `ansi-regex` from `5.0.1` to `6.0.1` +- f2dfbd3fb0: Adjust ErrorPage to accept optional supportUrl property to override app support config. Update type of additionalInfo property to be ReactNode to accept both string and component. +- 19155e0939: Updated React component type declarations to avoid exporting exotic component types. +- 89c84b9108: chore: fixing typescript errors for `TabbedCard.tsx` for React 17.x +- d62bdb7a8e: The `ErrorPage` now falls back to using the default support configuration if the `ConfigApi` is not available. + +## 0.8.8-next.0 + +### Patch Changes + +- 8d785a0b1b: chore: bump `ansi-regex` from `5.0.1` to `6.0.1` +- f2dfbd3fb0: Adjust ErrorPage to accept optional supportUrl property to override app support config. Update type of additionalInfo property to be ReactNode to accept both string and component. +- d62bdb7a8e: The `ErrorPage` now falls back to using the default support configuration if the `ConfigApi` is not available. + +## 0.8.7 + +### Patch Changes + +- f7257dff6f: The `` component now accepts a `noTrack` prop, which prevents the `click` event from being captured by the Analytics API. This can be used if tracking is explicitly not warranted, or in order to use custom link tracking in specific situations. +- 4c773ed25c: Change subtitle of Header style to use palette.bursts.fontColor +- f465b63b7f: Fix an issue where changes related to the `MobileSidebar` prevented scrolling pages. Additionally improve the menu of the `MobileSidebar` to not overlay the `BottomNavigation`. +- 064e750a50: Adding hover message to the Gauge and an info icon to the GaugeCard. +- a681cb9c2f: Make linkTarget configurable for MarkdownContent component + +## 0.8.7-next.1 + +### Patch Changes + +- f7257dff6f: The `` component now accepts a `noTrack` prop, which prevents the `click` event from being captured by the Analytics API. This can be used if tracking is explicitly not warranted, or in order to use custom link tracking in specific situations. + +## 0.8.7-next.0 + +### Patch Changes + +- 4c773ed25c: Change subtitle of Header style to use palette.bursts.fontColor +- f465b63b7f: Fix an issue where changes related to the `MobileSidebar` prevented scrolling pages. Additionally improve the menu of the `MobileSidebar` to not overlay the `BottomNavigation`. +- a681cb9c2f: Make linkTarget configurable for MarkdownContent component + ## 0.8.6 ### Patch Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 88e7f4efab..a8c51b6913 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -42,7 +42,7 @@ import { SessionApi } from '@backstage/core-plugin-api'; import { SignInPageProps } from '@backstage/core-plugin-api'; import { SparklinesLineProps } from 'react-sparklines'; import { SparklinesProps } from 'react-sparklines'; -import { StyledComponentProps } from '@material-ui/core/styles'; +import { StyledComponentProps } from '@material-ui/core/styles/withStyles'; import { StyleRules } from '@material-ui/styles'; import { StyleRules as StyleRules_2 } from '@material-ui/core/styles/withStyles'; import { TabProps } from '@material-ui/core/Tab'; @@ -120,7 +120,7 @@ export type BreadcrumbsStyledBoxClassKey = 'root'; export function BrokenImageIcon(props: IconComponentProps): JSX.Element; // @public -export function Button(props: ButtonProps): JSX.Element; +export const Button: (props: ButtonProps) => JSX.Element; // @public export type ButtonProps = ButtonProps_2 & Omit; @@ -384,7 +384,12 @@ export function GaugeCard(props: Props_10): JSX.Element; export type GaugeCardClassKey = 'root'; // @public (undocumented) -export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown'; +export type GaugeClassKey = + | 'root' + | 'overlay' + | 'description' + | 'circle' + | 'colorUnknown'; // @public (undocumented) export type GaugeProps = { @@ -393,6 +398,7 @@ export type GaugeProps = { inverse?: boolean; unit?: string; max?: number; + description?: ReactNode; getColor?: GaugePropsGetColor; }; @@ -588,10 +594,10 @@ export type LifecycleClassKey = 'alpha' | 'beta'; // @public (undocumented) export function LinearGauge(props: Props_11): JSX.Element | null; -// Warning: (ae-missing-release-tag) "LinkType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "Link" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export function Link(props: LinkProps): JSX.Element; +// @public +export const Link: (props: LinkProps) => JSX.Element; // Warning: (ae-missing-release-tag) "LinkProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -599,6 +605,7 @@ export function Link(props: LinkProps): JSX.Element; export type LinkProps = LinkProps_2 & LinkProps_3 & { component?: ElementType; + noTrack?: boolean; }; // Warning: (ae-missing-release-tag) "LoginRequestListItemClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -902,269 +909,9 @@ export type SidebarContextType = { // // @public (undocumented) export const SidebarDivider: React_2.ComponentType< - Pick< - React_2.DetailedHTMLProps< - React_2.HTMLAttributes, - HTMLHRElement - >, - | 'id' - | 'color' - | 'translate' - | 'hidden' - | 'dir' - | 'slot' - | 'style' - | 'title' - | 'accessKey' - | 'draggable' - | 'lang' - | 'prefix' - | 'children' - | 'contentEditable' - | 'inputMode' - | 'tabIndex' - | 'defaultChecked' - | 'defaultValue' - | 'suppressContentEditableWarning' - | 'suppressHydrationWarning' - | 'contextMenu' - | 'placeholder' - | 'spellCheck' - | 'radioGroup' - | 'role' - | 'about' - | 'datatype' - | 'inlist' - | 'property' - | 'resource' - | 'typeof' - | 'vocab' - | 'autoCapitalize' - | 'autoCorrect' - | 'autoSave' - | 'itemProp' - | 'itemScope' - | 'itemType' - | 'itemID' - | 'itemRef' - | 'results' - | 'security' - | 'unselectable' - | 'is' - | 'aria-activedescendant' - | 'aria-atomic' - | 'aria-autocomplete' - | 'aria-busy' - | 'aria-checked' - | 'aria-colcount' - | 'aria-colindex' - | 'aria-colspan' - | 'aria-controls' - | 'aria-current' - | 'aria-describedby' - | 'aria-details' - | 'aria-disabled' - | 'aria-dropeffect' - | 'aria-errormessage' - | 'aria-expanded' - | 'aria-flowto' - | 'aria-grabbed' - | 'aria-haspopup' - | 'aria-hidden' - | 'aria-invalid' - | 'aria-keyshortcuts' - | 'aria-label' - | 'aria-labelledby' - | 'aria-level' - | 'aria-live' - | 'aria-modal' - | 'aria-multiline' - | 'aria-multiselectable' - | 'aria-orientation' - | 'aria-owns' - | 'aria-placeholder' - | 'aria-posinset' - | 'aria-pressed' - | 'aria-readonly' - | 'aria-relevant' - | 'aria-required' - | 'aria-roledescription' - | 'aria-rowcount' - | 'aria-rowindex' - | 'aria-rowspan' - | 'aria-selected' - | 'aria-setsize' - | 'aria-sort' - | 'aria-valuemax' - | 'aria-valuemin' - | 'aria-valuenow' - | 'aria-valuetext' - | 'dangerouslySetInnerHTML' - | 'onCopy' - | 'onCopyCapture' - | 'onCut' - | 'onCutCapture' - | 'onPaste' - | 'onPasteCapture' - | 'onCompositionEnd' - | 'onCompositionEndCapture' - | 'onCompositionStart' - | 'onCompositionStartCapture' - | 'onCompositionUpdate' - | 'onCompositionUpdateCapture' - | 'onFocus' - | 'onFocusCapture' - | 'onBlur' - | 'onBlurCapture' - | 'onChange' - | 'onChangeCapture' - | 'onBeforeInput' - | 'onBeforeInputCapture' - | 'onInput' - | 'onInputCapture' - | 'onReset' - | 'onResetCapture' - | 'onSubmit' - | 'onSubmitCapture' - | 'onInvalid' - | 'onInvalidCapture' - | 'onLoad' - | 'onLoadCapture' - | 'onError' - | 'onErrorCapture' - | 'onKeyDown' - | 'onKeyDownCapture' - | 'onKeyPress' - | 'onKeyPressCapture' - | 'onKeyUp' - | 'onKeyUpCapture' - | 'onAbort' - | 'onAbortCapture' - | 'onCanPlay' - | 'onCanPlayCapture' - | 'onCanPlayThrough' - | 'onCanPlayThroughCapture' - | 'onDurationChange' - | 'onDurationChangeCapture' - | 'onEmptied' - | 'onEmptiedCapture' - | 'onEncrypted' - | 'onEncryptedCapture' - | 'onEnded' - | 'onEndedCapture' - | 'onLoadedData' - | 'onLoadedDataCapture' - | 'onLoadedMetadata' - | 'onLoadedMetadataCapture' - | 'onLoadStart' - | 'onLoadStartCapture' - | 'onPause' - | 'onPauseCapture' - | 'onPlay' - | 'onPlayCapture' - | 'onPlaying' - | 'onPlayingCapture' - | 'onProgress' - | 'onProgressCapture' - | 'onRateChange' - | 'onRateChangeCapture' - | 'onSeeked' - | 'onSeekedCapture' - | 'onSeeking' - | 'onSeekingCapture' - | 'onStalled' - | 'onStalledCapture' - | 'onSuspend' - | 'onSuspendCapture' - | 'onTimeUpdate' - | 'onTimeUpdateCapture' - | 'onVolumeChange' - | 'onVolumeChangeCapture' - | 'onWaiting' - | 'onWaitingCapture' - | 'onAuxClick' - | 'onAuxClickCapture' - | 'onClick' - | 'onClickCapture' - | 'onContextMenu' - | 'onContextMenuCapture' - | 'onDoubleClick' - | 'onDoubleClickCapture' - | 'onDrag' - | 'onDragCapture' - | 'onDragEnd' - | 'onDragEndCapture' - | 'onDragEnter' - | 'onDragEnterCapture' - | 'onDragExit' - | 'onDragExitCapture' - | 'onDragLeave' - | 'onDragLeaveCapture' - | 'onDragOver' - | 'onDragOverCapture' - | 'onDragStart' - | 'onDragStartCapture' - | 'onDrop' - | 'onDropCapture' - | 'onMouseDown' - | 'onMouseDownCapture' - | 'onMouseEnter' - | 'onMouseLeave' - | 'onMouseMove' - | 'onMouseMoveCapture' - | 'onMouseOut' - | 'onMouseOutCapture' - | 'onMouseOver' - | 'onMouseOverCapture' - | 'onMouseUp' - | 'onMouseUpCapture' - | 'onSelect' - | 'onSelectCapture' - | 'onTouchCancel' - | 'onTouchCancelCapture' - | 'onTouchEnd' - | 'onTouchEndCapture' - | 'onTouchMove' - | 'onTouchMoveCapture' - | 'onTouchStart' - | 'onTouchStartCapture' - | 'onPointerDown' - | 'onPointerDownCapture' - | 'onPointerMove' - | 'onPointerMoveCapture' - | 'onPointerUp' - | 'onPointerUpCapture' - | 'onPointerCancel' - | 'onPointerCancelCapture' - | 'onPointerEnter' - | 'onPointerEnterCapture' - | 'onPointerLeave' - | 'onPointerLeaveCapture' - | 'onPointerOver' - | 'onPointerOverCapture' - | 'onPointerOut' - | 'onPointerOutCapture' - | 'onGotPointerCapture' - | 'onGotPointerCaptureCapture' - | 'onLostPointerCapture' - | 'onLostPointerCaptureCapture' - | 'onScroll' - | 'onScrollCapture' - | 'onWheel' - | 'onWheelCapture' - | 'onAnimationStart' - | 'onAnimationStartCapture' - | 'onAnimationEnd' - | 'onAnimationEndCapture' - | 'onAnimationIteration' - | 'onAnimationIterationCapture' - | 'onTransitionEnd' - | 'onTransitionEndCapture' - | keyof React_2.ClassAttributes - > & - StyledComponentProps<'root'> & { - className?: string | undefined; - } + React_2.ClassAttributes & + React_2.HTMLAttributes & + StyledComponentProps<'root'> >; // Warning: (ae-missing-release-tag) "SidebarDividerClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1202,9 +949,7 @@ export type SidebarIntroClassKey = // Warning: (ae-missing-release-tag) "SidebarItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export const SidebarItem: React_2.ForwardRefExoticComponent< - SidebarItemProps & React_2.RefAttributes ->; +export const SidebarItem: (props: SidebarItemProps) => JSX.Element; // @public (undocumented) export type SidebarItemClassKey = @@ -1264,269 +1009,9 @@ export type SidebarProps = { // // @public (undocumented) export const SidebarScrollWrapper: React_2.ComponentType< - Pick< - React_2.DetailedHTMLProps< - React_2.HTMLAttributes, - HTMLDivElement - >, - | 'id' - | 'color' - | 'translate' - | 'hidden' - | 'dir' - | 'slot' - | 'style' - | 'title' - | 'accessKey' - | 'draggable' - | 'lang' - | 'prefix' - | 'children' - | 'contentEditable' - | 'inputMode' - | 'tabIndex' - | 'defaultChecked' - | 'defaultValue' - | 'suppressContentEditableWarning' - | 'suppressHydrationWarning' - | 'contextMenu' - | 'placeholder' - | 'spellCheck' - | 'radioGroup' - | 'role' - | 'about' - | 'datatype' - | 'inlist' - | 'property' - | 'resource' - | 'typeof' - | 'vocab' - | 'autoCapitalize' - | 'autoCorrect' - | 'autoSave' - | 'itemProp' - | 'itemScope' - | 'itemType' - | 'itemID' - | 'itemRef' - | 'results' - | 'security' - | 'unselectable' - | 'is' - | 'aria-activedescendant' - | 'aria-atomic' - | 'aria-autocomplete' - | 'aria-busy' - | 'aria-checked' - | 'aria-colcount' - | 'aria-colindex' - | 'aria-colspan' - | 'aria-controls' - | 'aria-current' - | 'aria-describedby' - | 'aria-details' - | 'aria-disabled' - | 'aria-dropeffect' - | 'aria-errormessage' - | 'aria-expanded' - | 'aria-flowto' - | 'aria-grabbed' - | 'aria-haspopup' - | 'aria-hidden' - | 'aria-invalid' - | 'aria-keyshortcuts' - | 'aria-label' - | 'aria-labelledby' - | 'aria-level' - | 'aria-live' - | 'aria-modal' - | 'aria-multiline' - | 'aria-multiselectable' - | 'aria-orientation' - | 'aria-owns' - | 'aria-placeholder' - | 'aria-posinset' - | 'aria-pressed' - | 'aria-readonly' - | 'aria-relevant' - | 'aria-required' - | 'aria-roledescription' - | 'aria-rowcount' - | 'aria-rowindex' - | 'aria-rowspan' - | 'aria-selected' - | 'aria-setsize' - | 'aria-sort' - | 'aria-valuemax' - | 'aria-valuemin' - | 'aria-valuenow' - | 'aria-valuetext' - | 'dangerouslySetInnerHTML' - | 'onCopy' - | 'onCopyCapture' - | 'onCut' - | 'onCutCapture' - | 'onPaste' - | 'onPasteCapture' - | 'onCompositionEnd' - | 'onCompositionEndCapture' - | 'onCompositionStart' - | 'onCompositionStartCapture' - | 'onCompositionUpdate' - | 'onCompositionUpdateCapture' - | 'onFocus' - | 'onFocusCapture' - | 'onBlur' - | 'onBlurCapture' - | 'onChange' - | 'onChangeCapture' - | 'onBeforeInput' - | 'onBeforeInputCapture' - | 'onInput' - | 'onInputCapture' - | 'onReset' - | 'onResetCapture' - | 'onSubmit' - | 'onSubmitCapture' - | 'onInvalid' - | 'onInvalidCapture' - | 'onLoad' - | 'onLoadCapture' - | 'onError' - | 'onErrorCapture' - | 'onKeyDown' - | 'onKeyDownCapture' - | 'onKeyPress' - | 'onKeyPressCapture' - | 'onKeyUp' - | 'onKeyUpCapture' - | 'onAbort' - | 'onAbortCapture' - | 'onCanPlay' - | 'onCanPlayCapture' - | 'onCanPlayThrough' - | 'onCanPlayThroughCapture' - | 'onDurationChange' - | 'onDurationChangeCapture' - | 'onEmptied' - | 'onEmptiedCapture' - | 'onEncrypted' - | 'onEncryptedCapture' - | 'onEnded' - | 'onEndedCapture' - | 'onLoadedData' - | 'onLoadedDataCapture' - | 'onLoadedMetadata' - | 'onLoadedMetadataCapture' - | 'onLoadStart' - | 'onLoadStartCapture' - | 'onPause' - | 'onPauseCapture' - | 'onPlay' - | 'onPlayCapture' - | 'onPlaying' - | 'onPlayingCapture' - | 'onProgress' - | 'onProgressCapture' - | 'onRateChange' - | 'onRateChangeCapture' - | 'onSeeked' - | 'onSeekedCapture' - | 'onSeeking' - | 'onSeekingCapture' - | 'onStalled' - | 'onStalledCapture' - | 'onSuspend' - | 'onSuspendCapture' - | 'onTimeUpdate' - | 'onTimeUpdateCapture' - | 'onVolumeChange' - | 'onVolumeChangeCapture' - | 'onWaiting' - | 'onWaitingCapture' - | 'onAuxClick' - | 'onAuxClickCapture' - | 'onClick' - | 'onClickCapture' - | 'onContextMenu' - | 'onContextMenuCapture' - | 'onDoubleClick' - | 'onDoubleClickCapture' - | 'onDrag' - | 'onDragCapture' - | 'onDragEnd' - | 'onDragEndCapture' - | 'onDragEnter' - | 'onDragEnterCapture' - | 'onDragExit' - | 'onDragExitCapture' - | 'onDragLeave' - | 'onDragLeaveCapture' - | 'onDragOver' - | 'onDragOverCapture' - | 'onDragStart' - | 'onDragStartCapture' - | 'onDrop' - | 'onDropCapture' - | 'onMouseDown' - | 'onMouseDownCapture' - | 'onMouseEnter' - | 'onMouseLeave' - | 'onMouseMove' - | 'onMouseMoveCapture' - | 'onMouseOut' - | 'onMouseOutCapture' - | 'onMouseOver' - | 'onMouseOverCapture' - | 'onMouseUp' - | 'onMouseUpCapture' - | 'onSelect' - | 'onSelectCapture' - | 'onTouchCancel' - | 'onTouchCancelCapture' - | 'onTouchEnd' - | 'onTouchEndCapture' - | 'onTouchMove' - | 'onTouchMoveCapture' - | 'onTouchStart' - | 'onTouchStartCapture' - | 'onPointerDown' - | 'onPointerDownCapture' - | 'onPointerMove' - | 'onPointerMoveCapture' - | 'onPointerUp' - | 'onPointerUpCapture' - | 'onPointerCancel' - | 'onPointerCancelCapture' - | 'onPointerEnter' - | 'onPointerEnterCapture' - | 'onPointerLeave' - | 'onPointerLeaveCapture' - | 'onPointerOver' - | 'onPointerOverCapture' - | 'onPointerOut' - | 'onPointerOutCapture' - | 'onGotPointerCapture' - | 'onGotPointerCaptureCapture' - | 'onLostPointerCapture' - | 'onLostPointerCaptureCapture' - | 'onScroll' - | 'onScrollCapture' - | 'onWheel' - | 'onWheelCapture' - | 'onAnimationStart' - | 'onAnimationStartCapture' - | 'onAnimationEnd' - | 'onAnimationEndCapture' - | 'onAnimationIteration' - | 'onAnimationIterationCapture' - | 'onTransitionEnd' - | 'onTransitionEndCapture' - | keyof React_2.ClassAttributes - > & - StyledComponentProps<'root'> & { - className?: string | undefined; - } + React_2.ClassAttributes & + React_2.HTMLAttributes & + StyledComponentProps<'root'> >; // Warning: (ae-forgotten-export) The symbol "SidebarSearchFieldProps" needs to be exported by the entry point index.d.ts @@ -1539,269 +1024,9 @@ export function SidebarSearchField(props: SidebarSearchFieldProps): JSX.Element; // // @public (undocumented) export const SidebarSpace: React_2.ComponentType< - Pick< - React_2.DetailedHTMLProps< - React_2.HTMLAttributes, - HTMLDivElement - >, - | 'id' - | 'color' - | 'translate' - | 'hidden' - | 'dir' - | 'slot' - | 'style' - | 'title' - | 'accessKey' - | 'draggable' - | 'lang' - | 'prefix' - | 'children' - | 'contentEditable' - | 'inputMode' - | 'tabIndex' - | 'defaultChecked' - | 'defaultValue' - | 'suppressContentEditableWarning' - | 'suppressHydrationWarning' - | 'contextMenu' - | 'placeholder' - | 'spellCheck' - | 'radioGroup' - | 'role' - | 'about' - | 'datatype' - | 'inlist' - | 'property' - | 'resource' - | 'typeof' - | 'vocab' - | 'autoCapitalize' - | 'autoCorrect' - | 'autoSave' - | 'itemProp' - | 'itemScope' - | 'itemType' - | 'itemID' - | 'itemRef' - | 'results' - | 'security' - | 'unselectable' - | 'is' - | 'aria-activedescendant' - | 'aria-atomic' - | 'aria-autocomplete' - | 'aria-busy' - | 'aria-checked' - | 'aria-colcount' - | 'aria-colindex' - | 'aria-colspan' - | 'aria-controls' - | 'aria-current' - | 'aria-describedby' - | 'aria-details' - | 'aria-disabled' - | 'aria-dropeffect' - | 'aria-errormessage' - | 'aria-expanded' - | 'aria-flowto' - | 'aria-grabbed' - | 'aria-haspopup' - | 'aria-hidden' - | 'aria-invalid' - | 'aria-keyshortcuts' - | 'aria-label' - | 'aria-labelledby' - | 'aria-level' - | 'aria-live' - | 'aria-modal' - | 'aria-multiline' - | 'aria-multiselectable' - | 'aria-orientation' - | 'aria-owns' - | 'aria-placeholder' - | 'aria-posinset' - | 'aria-pressed' - | 'aria-readonly' - | 'aria-relevant' - | 'aria-required' - | 'aria-roledescription' - | 'aria-rowcount' - | 'aria-rowindex' - | 'aria-rowspan' - | 'aria-selected' - | 'aria-setsize' - | 'aria-sort' - | 'aria-valuemax' - | 'aria-valuemin' - | 'aria-valuenow' - | 'aria-valuetext' - | 'dangerouslySetInnerHTML' - | 'onCopy' - | 'onCopyCapture' - | 'onCut' - | 'onCutCapture' - | 'onPaste' - | 'onPasteCapture' - | 'onCompositionEnd' - | 'onCompositionEndCapture' - | 'onCompositionStart' - | 'onCompositionStartCapture' - | 'onCompositionUpdate' - | 'onCompositionUpdateCapture' - | 'onFocus' - | 'onFocusCapture' - | 'onBlur' - | 'onBlurCapture' - | 'onChange' - | 'onChangeCapture' - | 'onBeforeInput' - | 'onBeforeInputCapture' - | 'onInput' - | 'onInputCapture' - | 'onReset' - | 'onResetCapture' - | 'onSubmit' - | 'onSubmitCapture' - | 'onInvalid' - | 'onInvalidCapture' - | 'onLoad' - | 'onLoadCapture' - | 'onError' - | 'onErrorCapture' - | 'onKeyDown' - | 'onKeyDownCapture' - | 'onKeyPress' - | 'onKeyPressCapture' - | 'onKeyUp' - | 'onKeyUpCapture' - | 'onAbort' - | 'onAbortCapture' - | 'onCanPlay' - | 'onCanPlayCapture' - | 'onCanPlayThrough' - | 'onCanPlayThroughCapture' - | 'onDurationChange' - | 'onDurationChangeCapture' - | 'onEmptied' - | 'onEmptiedCapture' - | 'onEncrypted' - | 'onEncryptedCapture' - | 'onEnded' - | 'onEndedCapture' - | 'onLoadedData' - | 'onLoadedDataCapture' - | 'onLoadedMetadata' - | 'onLoadedMetadataCapture' - | 'onLoadStart' - | 'onLoadStartCapture' - | 'onPause' - | 'onPauseCapture' - | 'onPlay' - | 'onPlayCapture' - | 'onPlaying' - | 'onPlayingCapture' - | 'onProgress' - | 'onProgressCapture' - | 'onRateChange' - | 'onRateChangeCapture' - | 'onSeeked' - | 'onSeekedCapture' - | 'onSeeking' - | 'onSeekingCapture' - | 'onStalled' - | 'onStalledCapture' - | 'onSuspend' - | 'onSuspendCapture' - | 'onTimeUpdate' - | 'onTimeUpdateCapture' - | 'onVolumeChange' - | 'onVolumeChangeCapture' - | 'onWaiting' - | 'onWaitingCapture' - | 'onAuxClick' - | 'onAuxClickCapture' - | 'onClick' - | 'onClickCapture' - | 'onContextMenu' - | 'onContextMenuCapture' - | 'onDoubleClick' - | 'onDoubleClickCapture' - | 'onDrag' - | 'onDragCapture' - | 'onDragEnd' - | 'onDragEndCapture' - | 'onDragEnter' - | 'onDragEnterCapture' - | 'onDragExit' - | 'onDragExitCapture' - | 'onDragLeave' - | 'onDragLeaveCapture' - | 'onDragOver' - | 'onDragOverCapture' - | 'onDragStart' - | 'onDragStartCapture' - | 'onDrop' - | 'onDropCapture' - | 'onMouseDown' - | 'onMouseDownCapture' - | 'onMouseEnter' - | 'onMouseLeave' - | 'onMouseMove' - | 'onMouseMoveCapture' - | 'onMouseOut' - | 'onMouseOutCapture' - | 'onMouseOver' - | 'onMouseOverCapture' - | 'onMouseUp' - | 'onMouseUpCapture' - | 'onSelect' - | 'onSelectCapture' - | 'onTouchCancel' - | 'onTouchCancelCapture' - | 'onTouchEnd' - | 'onTouchEndCapture' - | 'onTouchMove' - | 'onTouchMoveCapture' - | 'onTouchStart' - | 'onTouchStartCapture' - | 'onPointerDown' - | 'onPointerDownCapture' - | 'onPointerMove' - | 'onPointerMoveCapture' - | 'onPointerUp' - | 'onPointerUpCapture' - | 'onPointerCancel' - | 'onPointerCancelCapture' - | 'onPointerEnter' - | 'onPointerEnterCapture' - | 'onPointerLeave' - | 'onPointerLeaveCapture' - | 'onPointerOver' - | 'onPointerOverCapture' - | 'onPointerOut' - | 'onPointerOutCapture' - | 'onGotPointerCapture' - | 'onGotPointerCaptureCapture' - | 'onLostPointerCapture' - | 'onLostPointerCaptureCapture' - | 'onScroll' - | 'onScrollCapture' - | 'onWheel' - | 'onWheelCapture' - | 'onAnimationStart' - | 'onAnimationStartCapture' - | 'onAnimationEnd' - | 'onAnimationEndCapture' - | 'onAnimationIteration' - | 'onAnimationIterationCapture' - | 'onTransitionEnd' - | 'onTransitionEndCapture' - | keyof React_2.ClassAttributes - > & - StyledComponentProps<'root'> & { - className?: string | undefined; - } + React_2.ClassAttributes & + React_2.HTMLAttributes & + StyledComponentProps<'root'> >; // Warning: (ae-missing-release-tag) "SidebarSpaceClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1813,269 +1038,9 @@ export type SidebarSpaceClassKey = 'root'; // // @public (undocumented) export const SidebarSpacer: React_2.ComponentType< - Pick< - React_2.DetailedHTMLProps< - React_2.HTMLAttributes, - HTMLDivElement - >, - | 'id' - | 'color' - | 'translate' - | 'hidden' - | 'dir' - | 'slot' - | 'style' - | 'title' - | 'accessKey' - | 'draggable' - | 'lang' - | 'prefix' - | 'children' - | 'contentEditable' - | 'inputMode' - | 'tabIndex' - | 'defaultChecked' - | 'defaultValue' - | 'suppressContentEditableWarning' - | 'suppressHydrationWarning' - | 'contextMenu' - | 'placeholder' - | 'spellCheck' - | 'radioGroup' - | 'role' - | 'about' - | 'datatype' - | 'inlist' - | 'property' - | 'resource' - | 'typeof' - | 'vocab' - | 'autoCapitalize' - | 'autoCorrect' - | 'autoSave' - | 'itemProp' - | 'itemScope' - | 'itemType' - | 'itemID' - | 'itemRef' - | 'results' - | 'security' - | 'unselectable' - | 'is' - | 'aria-activedescendant' - | 'aria-atomic' - | 'aria-autocomplete' - | 'aria-busy' - | 'aria-checked' - | 'aria-colcount' - | 'aria-colindex' - | 'aria-colspan' - | 'aria-controls' - | 'aria-current' - | 'aria-describedby' - | 'aria-details' - | 'aria-disabled' - | 'aria-dropeffect' - | 'aria-errormessage' - | 'aria-expanded' - | 'aria-flowto' - | 'aria-grabbed' - | 'aria-haspopup' - | 'aria-hidden' - | 'aria-invalid' - | 'aria-keyshortcuts' - | 'aria-label' - | 'aria-labelledby' - | 'aria-level' - | 'aria-live' - | 'aria-modal' - | 'aria-multiline' - | 'aria-multiselectable' - | 'aria-orientation' - | 'aria-owns' - | 'aria-placeholder' - | 'aria-posinset' - | 'aria-pressed' - | 'aria-readonly' - | 'aria-relevant' - | 'aria-required' - | 'aria-roledescription' - | 'aria-rowcount' - | 'aria-rowindex' - | 'aria-rowspan' - | 'aria-selected' - | 'aria-setsize' - | 'aria-sort' - | 'aria-valuemax' - | 'aria-valuemin' - | 'aria-valuenow' - | 'aria-valuetext' - | 'dangerouslySetInnerHTML' - | 'onCopy' - | 'onCopyCapture' - | 'onCut' - | 'onCutCapture' - | 'onPaste' - | 'onPasteCapture' - | 'onCompositionEnd' - | 'onCompositionEndCapture' - | 'onCompositionStart' - | 'onCompositionStartCapture' - | 'onCompositionUpdate' - | 'onCompositionUpdateCapture' - | 'onFocus' - | 'onFocusCapture' - | 'onBlur' - | 'onBlurCapture' - | 'onChange' - | 'onChangeCapture' - | 'onBeforeInput' - | 'onBeforeInputCapture' - | 'onInput' - | 'onInputCapture' - | 'onReset' - | 'onResetCapture' - | 'onSubmit' - | 'onSubmitCapture' - | 'onInvalid' - | 'onInvalidCapture' - | 'onLoad' - | 'onLoadCapture' - | 'onError' - | 'onErrorCapture' - | 'onKeyDown' - | 'onKeyDownCapture' - | 'onKeyPress' - | 'onKeyPressCapture' - | 'onKeyUp' - | 'onKeyUpCapture' - | 'onAbort' - | 'onAbortCapture' - | 'onCanPlay' - | 'onCanPlayCapture' - | 'onCanPlayThrough' - | 'onCanPlayThroughCapture' - | 'onDurationChange' - | 'onDurationChangeCapture' - | 'onEmptied' - | 'onEmptiedCapture' - | 'onEncrypted' - | 'onEncryptedCapture' - | 'onEnded' - | 'onEndedCapture' - | 'onLoadedData' - | 'onLoadedDataCapture' - | 'onLoadedMetadata' - | 'onLoadedMetadataCapture' - | 'onLoadStart' - | 'onLoadStartCapture' - | 'onPause' - | 'onPauseCapture' - | 'onPlay' - | 'onPlayCapture' - | 'onPlaying' - | 'onPlayingCapture' - | 'onProgress' - | 'onProgressCapture' - | 'onRateChange' - | 'onRateChangeCapture' - | 'onSeeked' - | 'onSeekedCapture' - | 'onSeeking' - | 'onSeekingCapture' - | 'onStalled' - | 'onStalledCapture' - | 'onSuspend' - | 'onSuspendCapture' - | 'onTimeUpdate' - | 'onTimeUpdateCapture' - | 'onVolumeChange' - | 'onVolumeChangeCapture' - | 'onWaiting' - | 'onWaitingCapture' - | 'onAuxClick' - | 'onAuxClickCapture' - | 'onClick' - | 'onClickCapture' - | 'onContextMenu' - | 'onContextMenuCapture' - | 'onDoubleClick' - | 'onDoubleClickCapture' - | 'onDrag' - | 'onDragCapture' - | 'onDragEnd' - | 'onDragEndCapture' - | 'onDragEnter' - | 'onDragEnterCapture' - | 'onDragExit' - | 'onDragExitCapture' - | 'onDragLeave' - | 'onDragLeaveCapture' - | 'onDragOver' - | 'onDragOverCapture' - | 'onDragStart' - | 'onDragStartCapture' - | 'onDrop' - | 'onDropCapture' - | 'onMouseDown' - | 'onMouseDownCapture' - | 'onMouseEnter' - | 'onMouseLeave' - | 'onMouseMove' - | 'onMouseMoveCapture' - | 'onMouseOut' - | 'onMouseOutCapture' - | 'onMouseOver' - | 'onMouseOverCapture' - | 'onMouseUp' - | 'onMouseUpCapture' - | 'onSelect' - | 'onSelectCapture' - | 'onTouchCancel' - | 'onTouchCancelCapture' - | 'onTouchEnd' - | 'onTouchEndCapture' - | 'onTouchMove' - | 'onTouchMoveCapture' - | 'onTouchStart' - | 'onTouchStartCapture' - | 'onPointerDown' - | 'onPointerDownCapture' - | 'onPointerMove' - | 'onPointerMoveCapture' - | 'onPointerUp' - | 'onPointerUpCapture' - | 'onPointerCancel' - | 'onPointerCancelCapture' - | 'onPointerEnter' - | 'onPointerEnterCapture' - | 'onPointerLeave' - | 'onPointerLeaveCapture' - | 'onPointerOver' - | 'onPointerOverCapture' - | 'onPointerOut' - | 'onPointerOutCapture' - | 'onGotPointerCapture' - | 'onGotPointerCaptureCapture' - | 'onLostPointerCapture' - | 'onLostPointerCaptureCapture' - | 'onScroll' - | 'onScrollCapture' - | 'onWheel' - | 'onWheelCapture' - | 'onAnimationStart' - | 'onAnimationStartCapture' - | 'onAnimationEnd' - | 'onAnimationEndCapture' - | 'onAnimationIteration' - | 'onAnimationIterationCapture' - | 'onTransitionEnd' - | 'onTransitionEndCapture' - | keyof React_2.ClassAttributes - > & - StyledComponentProps<'root'> & { - className?: string | undefined; - } + React_2.ClassAttributes & + React_2.HTMLAttributes & + StyledComponentProps<'root'> >; // Warning: (ae-missing-release-tag) "SidebarSpacerClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 8037d52837..ecb910c50b 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.8.6", + "version": "0.8.8", "private": false, "publishConfig": { "access": "public", @@ -39,7 +39,7 @@ "@material-ui/lab": "4.0.0-alpha.57", "@types/react-sparklines": "^1.7.0", "@types/react-text-truncate": "^0.14.0", - "ansi-regex": "^5.0.1", + "ansi-regex": "^6.0.1", "classnames": "^2.2.6", "d3-selection": "^3.0.0", "d3-shape": "^3.0.0", @@ -73,9 +73,9 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/core-app-api": "^0.5.1", - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/core-app-api": "^0.5.2", + "@backstage/cli": "^0.13.2", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -94,7 +94,7 @@ "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", "@types/zen-observable": "^0.8.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/packages/core-components/src/components/Button/Button.tsx b/packages/core-components/src/components/Button/Button.tsx index 81c8ee28a5..d7d3c192a4 100644 --- a/packages/core-components/src/components/Button/Button.tsx +++ b/packages/core-components/src/components/Button/Button.tsx @@ -31,6 +31,13 @@ import { Link, LinkProps } from '../Link'; export type ButtonProps = MaterialButtonProps & Omit; +/** + * This wrapper is here to reset the color of the Link and make typescript happy. + */ +const LinkWrapper = React.forwardRef((props, ref) => ( + +)); + /** * Thin wrapper on top of material-ui's {@link https://v4.mui.com/components/buttons/ | Button} component * @@ -39,23 +46,6 @@ export type ButtonProps = MaterialButtonProps & * * Makes the Button to utilise react-router */ -declare function ButtonType(props: ButtonProps): JSX.Element; - -/** - * This wrapper is here to reset the color of the Link and make typescript happy. - */ -const LinkWrapper = React.forwardRef((props, ref) => ( - -)); - -/** @public */ -const ActualButton = React.forwardRef((props, ref) => ( +export const Button = React.forwardRef((props, ref) => ( -)) as { (props: ButtonProps): JSX.Element }; - -// TODO(Rugvip): We use this as a workaround to make the exported type be a -// function, which makes our API reference docs much nicer. -// The first type to be exported gets priority, but it will -// be thrown away when compiling to JS. -// @ts-ignore -export { ButtonType as Button, ActualButton as Button }; +)) as (props: ButtonProps) => JSX.Element; diff --git a/packages/core-components/src/components/Link/Link.test.tsx b/packages/core-components/src/components/Link/Link.test.tsx index 2d00aad827..8c6ed54d76 100644 --- a/packages/core-components/src/components/Link/Link.test.tsx +++ b/packages/core-components/src/components/Link/Link.test.tsx @@ -76,6 +76,33 @@ describe('', () => { }); }); + it('does not capture click when noTrack is set', async () => { + const linkText = 'Navigate!'; + const analyticsApi = new MockAnalyticsApi(); + const customOnClick = jest.fn(); + + const { getByText } = render( + wrapInTestApp( + + + {linkText} + + , + ), + ); + + fireEvent.click(getByText(linkText)); + + // Analytics event should have been fired. + await waitFor(() => { + // Custom onClick handler should have been fired. + expect(customOnClick).toHaveBeenCalled(); + + // But there should be no analytics event. + expect(analyticsApi.getEvents()).toHaveLength(0); + }); + }); + describe('isExternalUri', () => { it.each([ [true, 'http://'], diff --git a/packages/core-components/src/components/Link/Link.tsx b/packages/core-components/src/components/Link/Link.tsx index 8866150f86..a1af06ae3b 100644 --- a/packages/core-components/src/components/Link/Link.tsx +++ b/packages/core-components/src/components/Link/Link.tsx @@ -29,10 +29,9 @@ export const isExternalUri = (uri: string) => /^([a-z+.-]+):/.test(uri); export type LinkProps = MaterialLinkProps & RouterLinkProps & { component?: ElementType; + noTrack?: boolean; }; -declare function LinkType(props: LinkProps): JSX.Element; - /** * Given a react node, try to retrieve its text content. */ @@ -61,8 +60,8 @@ const getNodeText = (node: React.ReactNode): string => { * - Makes the Link use react-router * - Captures Link clicks as analytics events. */ -const ActualLink = React.forwardRef( - ({ onClick, ...props }, ref) => { +export const Link = React.forwardRef( + ({ onClick, noTrack, ...props }, ref) => { const analytics = useAnalytics(); const to = String(props.to); const linkText = getNodeText(props.children) || to; @@ -71,7 +70,9 @@ const ActualLink = React.forwardRef( const handleClick = (event: React.MouseEvent) => { onClick?.(event); - analytics.captureEvent('click', linkText, { attributes: { to } }); + if (!noTrack) { + analytics.captureEvent('click', linkText, { attributes: { to } }); + } }; return external ? ( @@ -93,11 +94,4 @@ const ActualLink = React.forwardRef( /> ); }, -); - -// TODO(Rugvip): We use this as a workaround to make the exported type be a -// function, which makes our API reference docs much nicer. -// The first type to be exported gets priority, but it will -// be thrown away when compiling to JS. -// @ts-ignore -export { LinkType as Link, ActualLink as Link }; +) as (props: LinkProps) => JSX.Element; diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx index 0336ebc990..11f977b387 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx @@ -17,10 +17,15 @@ import { BackstagePalette, BackstageTheme } from '@backstage/theme'; import { makeStyles, useTheme } from '@material-ui/core/styles'; import { Circle } from 'rc-progress'; -import React from 'react'; +import React, { ReactNode, useEffect, useState } from 'react'; /** @public */ -export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown'; +export type GaugeClassKey = + | 'root' + | 'overlay' + | 'description' + | 'circle' + | 'colorUnknown'; const useStyles = makeStyles( theme => ({ @@ -37,6 +42,15 @@ const useStyles = makeStyles( fontWeight: 'bold', color: theme.palette.textContrast, }, + description: { + fontSize: '100%', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + position: 'absolute', + wordBreak: 'break-all', + display: 'inline-block', + }, circle: { width: '80%', transform: 'translate(10%, 0)', @@ -53,6 +67,7 @@ export type GaugeProps = { inverse?: boolean; unit?: string; max?: number; + description?: ReactNode; getColor?: GaugePropsGetColor; }; @@ -104,10 +119,11 @@ export const getProgressColor: GaugePropsGetColor = ({ */ export function Gauge(props: GaugeProps) { + const [hoverRef, setHoverRef] = useState(null); const { getColor = getProgressColor } = props; const classes = useStyles(props); const { palette } = useTheme(); - const { value, fractional, inverse, unit, max } = { + const { value, fractional, inverse, unit, max, description } = { ...defaultGaugeProps, ...props, }; @@ -115,8 +131,28 @@ export function Gauge(props: GaugeProps) { const asPercentage = fractional ? Math.round(value * max) : value; const asActual = max !== 100 ? Math.round(value) : asPercentage; + const [isHovering, setIsHovering] = useState(false); + + useEffect(() => { + const node = hoverRef; + const handleMouseOver = () => setIsHovering(true); + const handleMouseOut = () => setIsHovering(false); + if (node && description) { + node.addEventListener('mouseenter', handleMouseOver); + node.addEventListener('mouseleave', handleMouseOut); + + return () => { + node.removeEventListener('mouseenter', handleMouseOver); + node.removeEventListener('mouseleave', handleMouseOut); + }; + } + return () => { + setIsHovering(false); + }; + }, [description, hoverRef]); + return ( -
+
-
- {isNaN(value) ? 'N/A' : `${asActual}${unit}`} -
+ {description && isHovering ? ( +
{description}
+ ) : ( +
+ {isNaN(value) ? 'N/A' : `${asActual}${unit}`} +
+ )}
); } diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx index 64a6cc1b16..08c7813643 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx @@ -18,6 +18,8 @@ import React, { PropsWithChildren } from 'react'; import { GaugeCard } from './GaugeCard'; import Grid from '@material-ui/core/Grid'; import { MemoryRouter } from 'react-router'; +import Tooltip from '@material-ui/core/Tooltip'; +import Info from '@material-ui/icons/Info'; const linkInfo = { title: 'Go to XYZ Location', link: '#' }; @@ -118,3 +120,79 @@ export const StaticColor = () => ( ); + +export const InfoMessage = () => ( + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + + + + + } + /> + + +); + +export const HoverMessage = () => ( + + + + + + + + + + + + + + +); diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx index 9e6c19ebfe..1114890523 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx @@ -15,7 +15,7 @@ */ import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { BottomLinkProps } from '../../layout/BottomLink'; import { InfoCard, InfoCardVariants } from '../../layout/InfoCard'; import { Gauge, GaugePropsGetColor } from './Gauge'; @@ -26,6 +26,8 @@ type Props = { variant?: InfoCardVariants; /** Progress in % specified as decimal, e.g. "0.23" */ progress: number; + description?: ReactNode; + icon?: ReactNode; inverse?: boolean; deepLink?: BottomLinkProps; getColor?: GaugePropsGetColor; @@ -52,11 +54,21 @@ const useStyles = makeStyles( */ export function GaugeCard(props: Props) { const classes = useStyles(props); - const { title, subheader, progress, inverse, deepLink, variant, getColor } = - props; + const { + title, + subheader, + progress, + inverse, + deepLink, + description, + icon, + variant, + getColor, + } = props; const gaugeProps = { inverse, + description, getColor, value: progress, }; @@ -68,6 +80,7 @@ export function GaugeCard(props: Props) { subheader={subheader} deepLink={deepLink} variant={variant} + icon={icon} > diff --git a/packages/core-components/src/hooks/useSupportConfig.ts b/packages/core-components/src/hooks/useSupportConfig.ts index 5d64e38f3d..546d779e01 100644 --- a/packages/core-components/src/hooks/useSupportConfig.ts +++ b/packages/core-components/src/hooks/useSupportConfig.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useApi, configApiRef } from '@backstage/core-plugin-api'; +import { useApiHolder, configApiRef } from '@backstage/core-plugin-api'; export type SupportItemLink = { url: string; @@ -50,8 +50,9 @@ const DEFAULT_SUPPORT_CONFIG: SupportConfig = { }; export function useSupportConfig(): SupportConfig { - const config = useApi(configApiRef); - const supportConfig = config.getOptionalConfig('app.support'); + const apiHolder = useApiHolder(); + const config = apiHolder.get(configApiRef); + const supportConfig = config?.getOptionalConfig('app.support'); if (!supportConfig) { return DEFAULT_SUPPORT_CONFIG; diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx index a314128bce..2efdc5da33 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { ErrorPage } from './ErrorPage'; +import { Link } from '../../components/Link'; import { renderInTestApp } from '@backstage/test-utils'; describe('', () => { @@ -30,4 +31,72 @@ describe('', () => { ).toBeInTheDocument(); expect(getByTestId('go-back-link')).toBeInTheDocument(); }); + + it('should render with additional information of type string', async () => { + const { getByText } = await renderInTestApp( + , + ); + expect( + getByText(/looks like someone dropped the mic!/i), + ).toBeInTheDocument(); + expect( + getByText(/This is a string based additional information/i), + ).toBeInTheDocument(); + }); + + it('should render with additional information including link', async () => { + const { getByText } = await renderInTestApp( + + This is some additional information including{' '} + a link + + } + />, + ); + expect( + getByText(/looks like someone dropped the mic!/i), + ).toBeInTheDocument(); + expect(getByText(/a link/i)).toBeInTheDocument(); + expect(getByText(/a link/i)).toHaveAttribute('href', '/test'); + }); + + it('should render with default support url if supportUrl is not provided', async () => { + const { getByText } = await renderInTestApp( + , + ); + expect( + getByText(/looks like someone dropped the mic!/i), + ).toBeInTheDocument(); + expect(getByText(/contact support/i)).toBeInTheDocument(); + expect(getByText(/contact support/i)).toHaveAttribute( + 'href', + 'https://github.com/backstage/backstage/issues', + ); + }); + + it('should override support url if supportUrl property is provided', async () => { + const { getByText } = await renderInTestApp( + , + ); + expect( + getByText(/looks like someone dropped the mic!/i), + ).toBeInTheDocument(); + expect(getByText(/contact support/i)).toBeInTheDocument(); + expect(getByText(/contact support/i)).toHaveAttribute( + 'href', + 'https://error-page-test-support-url.com', + ); + }); }); diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx index 08c002d849..928a3eb0d4 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx @@ -27,7 +27,8 @@ import { MicDrop } from './MicDrop'; interface IErrorPageProps { status: string; statusMessage: string; - additionalInfo?: string; + additionalInfo?: React.ReactNode; + supportUrl?: string; } /** @public */ @@ -62,7 +63,7 @@ const useStyles = makeStyles( * */ export function ErrorPage(props: IErrorPageProps) { - const { status, statusMessage, additionalInfo } = props; + const { status, statusMessage, additionalInfo, supportUrl } = props; const classes = useStyles(); const navigate = useNavigate(); const support = useSupportConfig(); @@ -88,7 +89,8 @@ export function ErrorPage(props: IErrorPageProps) { navigate(-1)}> Go back - ... or please contact support if you + ... or please{' '} + contact support if you think this is a bug. diff --git a/packages/core-components/src/layout/InfoCard/InfoCard.tsx b/packages/core-components/src/layout/InfoCard/InfoCard.tsx index f1e67904f1..256d60cfa2 100644 --- a/packages/core-components/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core-components/src/layout/InfoCard/InfoCard.tsx @@ -55,6 +55,9 @@ const useStyles = makeStyles( headerAvatar: {}, headerAction: {}, headerContent: {}, + subheader: { + display: 'flex', + }, }), { name: 'BackstageInfoCard' }, ); @@ -134,6 +137,7 @@ type Props = { children?: ReactNode; headerStyle?: object; headerProps?: CardHeaderProps; + icon?: ReactNode; action?: ReactNode; actionsClassName?: string; actions?: ReactNode; @@ -162,6 +166,7 @@ export function InfoCard(props: Props): JSX.Element { children, headerStyle, headerProps, + icon, action, actionsClassName, actions, @@ -194,6 +199,15 @@ export function InfoCard(props: Props): JSX.Element { }); } + const cardSubTitle = () => { + return ( +
+ {subheader &&
{subheader}
} + {icon} +
+ ); + }; + const errProps: ErrorBoundaryProps = errorBoundaryProps || (slackChannel ? { slackChannel } : {}); @@ -211,7 +225,7 @@ export function InfoCard(props: Props): JSX.Element { content: classes.headerContent, }} title={title} - subheader={subheader} + subheader={cardSubTitle()} action={action} style={{ ...headerStyle }} titleTypographyProps={titleTypographyProps} diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index fc97fdc0ce..7eb86772f9 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -21,13 +21,18 @@ import useMediaQuery from '@material-ui/core/useMediaQuery'; import Badge from '@material-ui/core/Badge'; import TextField from '@material-ui/core/TextField'; import Typography from '@material-ui/core/Typography'; -import { CreateCSSProperties } from '@material-ui/core/styles/withStyles'; +import { + CreateCSSProperties, + StyledComponentProps, +} from '@material-ui/core/styles/withStyles'; import ArrowRightIcon from '@material-ui/icons/ArrowRight'; import SearchIcon from '@material-ui/icons/Search'; import ArrowDropUp from '@material-ui/icons/ArrowDropUp'; import ArrowDropDown from '@material-ui/icons/ArrowDropDown'; import classnames from 'classnames'; import React, { + ComponentProps, + ComponentType, forwardRef, KeyboardEventHandler, ReactNode, @@ -504,7 +509,7 @@ export const SidebarItem = forwardRef((props, ref) => { } return ; -}); +}) as (props: SidebarItemProps) => JSX.Element; type SidebarSearchFieldProps = { onSearch: (input: string) => void; @@ -580,7 +585,7 @@ export const SidebarSpace = styled('div')( flex: 1, }, { name: 'BackstageSidebarSpace' }, -); +) as ComponentType & StyledComponentProps<'root'>>; export type SidebarSpacerClassKey = 'root'; @@ -589,7 +594,7 @@ export const SidebarSpacer = styled('div')( height: 8, }, { name: 'BackstageSidebarSpacer' }, -); +) as ComponentType & StyledComponentProps<'root'>>; export type SidebarDividerClassKey = 'root'; @@ -602,7 +607,7 @@ export const SidebarDivider = styled('hr')( margin: '12px 0px', }, { name: 'BackstageSidebarDivider' }, -); +) as ComponentType & StyledComponentProps<'root'>>; const styledScrollbar = (theme: Theme): CreateCSSProperties => ({ overflowY: 'auto', @@ -631,7 +636,7 @@ export const SidebarScrollWrapper = styled('div')(({ theme }) => { '@media (hover: none)': scrollbarStyles, '&:hover': scrollbarStyles, }; -}); +}) as ComponentType & StyledComponentProps<'root'>>; /** * A button which allows you to expand the sidebar when clicked. diff --git a/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx b/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx index 06e9f53224..b3b977f04b 100644 --- a/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx +++ b/packages/core-components/src/layout/TabbedCard/TabbedCard.tsx @@ -89,12 +89,15 @@ export function TabbedCard(props: PropsWithChildren) { let selectedTabContent: ReactNode; if (!value) { React.Children.map(children, (child, index) => { - if (index === selectedIndex) selectedTabContent = child?.props.children; + if (React.isValidElement(child) && index === selectedIndex) { + selectedTabContent = child?.props.children; + } }); } else { React.Children.map(children, child => { - if (child?.props.value === value) + if (React.isValidElement(child) && child?.props.value === value) { selectedTabContent = child?.props.children; + } }); } diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 10146ee8e4..c9f110b432 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -43,9 +43,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2-next.0", + "@backstage/core-app-api": "^0.5.2-next.0", + "@backstage/test-utils": "^0.2.4-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -54,7 +54,7 @@ "@types/node": "^14.14.32", "@types/prop-types": "^15.7.3", "@types/zen-observable": "^0.8.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 3c426293af..886d873740 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,320 @@ # @backstage/create-app +## 0.4.19 + +### Patch Changes + +- 22f4ecb0e6: Switched the `file:` dependency for a `link:` dependency in the `backend` package. This makes sure that the `app` package is linked in rather than copied. + + To apply this update to an existing app, make the following change to `packages/backend/package.json`: + + ```diff + "dependencies": { + - "app": "file:../app", + + "app": "link:../app", + "@backstage/backend-common": "^{{version '@backstage/backend-common'}}", + ``` + +- 1dd5a02e91: **BREAKING:** Updated `knex` to major version 1, which also implies changing out + the underlying `sqlite` implementation. + + The old `sqlite3` NPM library has been abandoned by its maintainers, which has + led to unhandled security reports and other issues. Therefore, in the `knex` 1.x + release line they have instead switched over to the [`@vscode/sqlite3` + library](https://github.com/microsoft/vscode-node-sqlite3) by default, which is + actively maintained by Microsoft. + + This means that as you update to this version of Backstage, there are two + breaking changes that you will have to address in your own repository: + + ## Bumping `knex` itself + + All `package.json` files of your repo that used to depend on a 0.x version of + `knex`, should now be updated to depend on the 1.x release line. This applies in + particular to `packages/backend`, but may also occur in backend plugins or + libraries. + + ```diff + - "knex": "^0.95.1", + + "knex": "^1.0.2", + ``` + + Almost all existing database code will continue to function without modification + after this bump. The only significant difference that we discovered in the main + repo, is that the `alter()` function had a slightly different signature in + migration files. It now accepts an object with `alterType` and `alterNullable` + fields that clarify a previous grey area such that the intent of the alteration + is made explicit. This is caught by `tsc` and your editor if you are using the + `@ts-check` and `@param` syntax in your migration files + ([example](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)), + which we strongly recommend. + + See the [`knex` documentation](https://knexjs.org/#Schema-alter) for more + information about the `alter` syntax. + + Also see the [`knex` changelog](https://knexjs.org/#changelog) for information + about breaking changes in the 1.x line; if you are using `RETURNING` you may + want to make some additional modifications in your code. + + ## Switching out `sqlite3` + + All `package.json` files of your repo that used to depend on `sqlite3`, should + now be updated to depend on `@vscode/sqlite3`. This applies in particular to + `packages/backend`, but may also occur in backend plugins or libraries. + + ```diff + - "sqlite3": "^5.0.1", + + "@vscode/sqlite3": "^5.0.7", + ``` + + These should be functionally equivalent, except that the new library will have + addressed some long standing problems with old transitive dependencies etc. + +## 0.4.19-next.0 + +### Patch Changes + +- 22f4ecb0e6: Switched the `file:` dependency for a `link:` dependency in the `backend` package. This makes sure that the `app` package is linked in rather than copied. + + To apply this update to an existing app, make the following change to `packages/backend/package.json`: + + ```diff + "dependencies": { + - "app": "file:../app", + + "app": "link:../app", + "@backstage/backend-common": "^{{version '@backstage/backend-common'}}", + ``` + +- 1dd5a02e91: **BREAKING:** Updated `knex` to major version 1, which also implies changing out + the underlying `sqlite` implementation. + + The old `sqlite3` NPM library has been abandoned by its maintainers, which has + led to unhandled security reports and other issues. Therefore, in the `knex` 1.x + release line they have instead switched over to the [`@vscode/sqlite3` + library](https://github.com/microsoft/vscode-node-sqlite3) by default, which is + actively maintained by Microsoft. + + This means that as you update to this version of Backstage, there are two + breaking changes that you will have to address in your own repository: + + ## Bumping `knex` itself + + All `package.json` files of your repo that used to depend on a 0.x version of + `knex`, should now be updated to depend on the 1.x release line. This applies in + particular to `packages/backend`, but may also occur in backend plugins or + libraries. + + ```diff + - "knex": "^0.95.1", + + "knex": "^1.0.2", + ``` + + Almost all existing database code will continue to function without modification + after this bump. The only significant difference that we discovered in the main + repo, is that the `alter()` function had a slightly different signature in + migration files. It now accepts an object with `alterType` and `alterNullable` + fields that clarify a previous grey area such that the intent of the alteration + is made explicit. This is caught by `tsc` and your editor if you are using the + `@ts-check` and `@param` syntax in your migration files + ([example](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)), + which we strongly recommend. + + See the [`knex` documentation](https://knexjs.org/#Schema-alter) for more + information about the `alter` syntax. + + Also see the [`knex` changelog](https://knexjs.org/#changelog) for information + about breaking changes in the 1.x line; if you are using `RETURNING` you may + want to make some additional modifications in your code. + + ## Switching out `sqlite3` + + All `package.json` files of your repo that used to depend on `sqlite3`, should + now be updated to depend on `@vscode/sqlite3`. This applies in particular to + `packages/backend`, but may also occur in backend plugins or libraries. + + ```diff + - "sqlite3": "^5.0.1", + + "@vscode/sqlite3": "^5.0.7", + ``` + + These should be functionally equivalent, except that the new library will have + addressed some long standing problems with old transitive dependencies etc. + +## 0.4.18 + +### Patch Changes + +- 5bd0ce9e62: chore(deps): bump `inquirer` from 7.3.3 to 8.2.0 +- f27f5197e2: Apply the fix from `0.4.16`, which is part of the `v0.65.1` release of Backstage. +- 2687029a67: Update backend-to-backend auth link in configuration file comment +- 24ef62048c: Adds missing `/catalog-graph` route to ``. + + To fix this problem for a recently created app please update your `app/src/App.tsx` + + ```diff + + import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; + + ... omitted ... + + + } /> + + } /> + + ``` + +- ba59832aed: Permission the `catalog-import` route + + The following changes are **required** if you intend to add permissions to your existing app. + + Use the `PermissionedRoute` for `CatalogImportPage` instead of the normal `Route`: + + ```diff + // packages/app/src/App.tsx + ... + + import { PermissionedRoute } from '@backstage/plugin-permission-react'; + + import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; + + ... + + - } /> + + } + + /> + ``` + +- cef64b1561: Added `tokenManager` as a required property for the auth-backend `createRouter` function. This dependency is used to issue server tokens that are used by the `CatalogIdentityClient` when looking up users and their group membership during authentication. + + These changes are **required** to `packages/backend/src/plugins/auth.ts`: + + ```diff + export default async function createPlugin({ + logger, + database, + config, + discovery, + + tokenManager, + }: PluginEnvironment): Promise { + return await createRouter({ + logger, + config, + database, + discovery, + + tokenManager, + }); + } + ``` + +- e39d88bd84: Switched the `app` dependency in the backend to use a file target rather than version. + + To apply this change to an existing app, make the following change to `packages/backend/package.json`: + + ```diff + "dependencies": { + - "app": "0.0.0", + + "app": "file:../app", + ``` + +## 0.4.18-next.1 + +### Patch Changes + +- 5bd0ce9e62: chore(deps): bump `inquirer` from 7.3.3 to 8.2.0 +- ba59832aed: Permission the `catalog-import` route + + The following changes are **required** if you intend to add permissions to your existing app. + + Use the `PermissionedRoute` for `CatalogImportPage` instead of the normal `Route`: + + ```diff + // packages/app/src/App.tsx + ... + + import { PermissionedRoute } from '@backstage/plugin-permission-react'; + + import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; + + ... + + - } /> + + } + + /> + ``` + +## 0.4.18-next.0 + +### Patch Changes + +- f27f5197e2: Apply the fix from `0.4.16`, which is part of the `v0.65.1` release of Backstage. +- 2687029a67: Update backend-to-backend auth link in configuration file comment +- 24ef62048c: Adds missing `/catalog-graph` route to ``. + + To fix this problem for a recently created app please update your `app/src/App.tsx` + + ```diff + + import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; + + ... omitted ... + + + } /> + + } /> + + ``` + +- cef64b1561: Added `tokenManager` as a required property for the auth-backend `createRouter` function. This dependency is used to issue server tokens that are used by the `CatalogIdentityClient` when looking up users and their group membership during authentication. + + These changes are **required** to `packages/backend/src/plugins/auth.ts`: + + ```diff + export default async function createPlugin({ + logger, + database, + config, + discovery, + + tokenManager, + }: PluginEnvironment): Promise { + return await createRouter({ + logger, + config, + database, + discovery, + + tokenManager, + }); + } + ``` + +- e39d88bd84: Switched the `app` dependency in the backend to use a file target rather than version. + + To apply this change to an existing app, make the following change to `packages/backend/package.json`: + + ```diff + "dependencies": { + - "app": "0.0.0", + + "app": "file:../app", + ``` + +## 0.4.16 + +### Patch Changes + +- c945cd9f7e: Adds missing `/catalog-graph` route to ``. + + To fix this problem for a recently created app please update your `app/src/App.tsx` + + ```diff + + import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; + + ... omitted ... + + + } /> + + } /> + + ``` + ## 0.4.15 ### Patch Changes @@ -26,11 +341,18 @@ } ``` +The `.fromConfig` of the `DefaultCatalogCollator` also now takes a `tokenManager` as a parameter. + +```diff +- collator: DefaultCatalogCollator.fromConfig(config, { discovery }), ++ collator: DefaultCatalogCollator.fromConfig(config, { discovery, tokenManager }), +``` + - a0d446c8ec: Replaced EntitySystemDiagramCard with EntityCatalogGraphCard To make this change to an existing app: - Add `@backstage/catalog-graph-plugin` as a `dependency` in `packages/app/package.json` + Add `@backstage/plugin-catalog-graph` as a `dependency` in `packages/app/package.json` or `cd packages/app && yarn add @backstage/plugin-catalog-graph`. Apply the following changes to the `packages/app/src/components/catalog/EntityPage.tsx` file: diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 3d7dfead49..4ab263abfd 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.15", + "version": "0.4.19", "private": false, "publishConfig": { "access": "public" @@ -35,7 +35,7 @@ "commander": "^6.1.0", "fs-extra": "9.1.0", "handlebars": "^4.7.3", - "inquirer": "^7.0.4", + "inquirer": "^8.2.0", "ora": "^5.3.0", "recursive-readdir": "^2.2.2" }, @@ -48,6 +48,7 @@ "ts-node": "^10.0.0" }, "peerDependencies": { + "@backstage/app-defaults": "", "@backstage/backend-common": "", "@backstage/backend-tasks": "", "@backstage/catalog-client": "", @@ -58,30 +59,35 @@ "@backstage/core-components": "", "@backstage/core-plugin-api": "", "@backstage/errors": "", + "@backstage/integration-react": "", "@backstage/plugin-api-docs": "", "@backstage/plugin-app-backend": "", "@backstage/plugin-auth-backend": "", "@backstage/plugin-catalog": "", "@backstage/plugin-catalog-backend": "", + "@backstage/plugin-catalog-common": "", "@backstage/plugin-catalog-graph": "", "@backstage/plugin-catalog-import": "", + "@backstage/plugin-catalog-react": "", + "@backstage/plugin-circleci": "", "@backstage/plugin-explore": "", "@backstage/plugin-github-actions": "", "@backstage/plugin-lighthouse": "", + "@backstage/plugin-org": "", "@backstage/plugin-permission-common": "", "@backstage/plugin-permission-node": "", + "@backstage/plugin-permission-react": "", "@backstage/plugin-proxy-backend": "", "@backstage/plugin-rollbar-backend": "", "@backstage/plugin-scaffolder": "", + "@backstage/plugin-scaffolder-backend": "", "@backstage/plugin-search": "", "@backstage/plugin-search-backend": "", "@backstage/plugin-search-backend-node": "", - "@backstage/plugin-scaffolder-backend": "", "@backstage/plugin-tech-radar": "", "@backstage/plugin-techdocs": "", "@backstage/plugin-techdocs-backend": "", "@backstage/plugin-user-settings": "", - "@backstage/integration-react": "", "@backstage/test-utils": "", "@backstage/theme": "" }, diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 24df97c694..d7b4692aa4 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -208,6 +208,6 @@ describe('templatingTask', () => { // backend dependencies include `sqlite3` from `context.SQLite` expect( fs.readFileSync('templatedApp/packages/backend/package.json', 'utf-8'), - ).toContain('"sqlite3"'); + ).toContain('sqlite3"'); }); }); diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index 34d016ebde..7179b7924f 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -49,6 +49,7 @@ import { version as pluginApiDocs } from '../../../../plugins/api-docs/package.j import { version as pluginAppBackend } from '../../../../plugins/app-backend/package.json'; import { version as pluginAuthBackend } from '../../../../plugins/auth-backend/package.json'; import { version as pluginCatalog } from '../../../../plugins/catalog/package.json'; +import { version as pluginCatalogCommon } from '../../../../plugins/catalog-common/package.json'; import { version as pluginCatalogReact } from '../../../../plugins/catalog-react/package.json'; import { version as pluginCatalogBackend } from '../../../../plugins/catalog-backend/package.json'; import { version as pluginCatalogGraph } from '../../../../plugins/catalog-graph/package.json'; @@ -59,6 +60,7 @@ import { version as pluginGithubActions } from '../../../../plugins/github-actio import { version as pluginLighthouse } from '../../../../plugins/lighthouse/package.json'; import { version as pluginOrg } from '../../../../plugins/org/package.json'; import { version as pluginPermissionCommon } from '../../../../plugins/permission-common/package.json'; +import { version as pluginPermissionReact } from '../../../../plugins/permission-react/package.json'; import { version as pluginPermissionNode } from '../../../../plugins/permission-node/package.json'; import { version as pluginProxyBackend } from '../../../../plugins/proxy-backend/package.json'; import { version as pluginRollbarBackend } from '../../../../plugins/rollbar-backend/package.json'; @@ -89,6 +91,7 @@ export const packageVersions = { '@backstage/plugin-app-backend': pluginAppBackend, '@backstage/plugin-auth-backend': pluginAuthBackend, '@backstage/plugin-catalog': pluginCatalog, + '@backstage/plugin-catalog-common': pluginCatalogCommon, '@backstage/plugin-catalog-react': pluginCatalogReact, '@backstage/plugin-catalog-backend': pluginCatalogBackend, '@backstage/plugin-catalog-graph': pluginCatalogGraph, @@ -100,6 +103,7 @@ export const packageVersions = { '@backstage/plugin-org': pluginOrg, '@backstage/plugin-permission-common': pluginPermissionCommon, '@backstage/plugin-permission-node': pluginPermissionNode, + '@backstage/plugin-permission-react': pluginPermissionReact, '@backstage/plugin-proxy-backend': pluginProxyBackend, '@backstage/plugin-rollbar-backend': pluginRollbarBackend, '@backstage/plugin-scaffolder': pluginScaffolder, diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 46e2ecbb05..618c06a0c3 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -7,7 +7,8 @@ organization: backend: # Used for enabling authentication, secret is shared by all backend plugins - # See backend-to-backend-auth.md in the docs for information on the format + # See https://backstage.io/docs/tutorials/backend-to-backend-auth for + # information on the format # auth: # keys: # - secret: ${BACKEND_SECRET} diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index 23c45d3f9d..53e0590db4 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -13,11 +13,13 @@ "@backstage/integration-react": "^{{version '@backstage/integration-react'}}", "@backstage/plugin-api-docs": "^{{version '@backstage/plugin-api-docs'}}", "@backstage/plugin-catalog": "^{{version '@backstage/plugin-catalog'}}", + "@backstage/plugin-catalog-common": "^{{version '@backstage/plugin-catalog-common'}}", "@backstage/plugin-catalog-graph": "^{{version '@backstage/plugin-catalog-graph'}}", "@backstage/plugin-catalog-import": "^{{version '@backstage/plugin-catalog-import'}}", "@backstage/plugin-catalog-react": "^{{version '@backstage/plugin-catalog-react'}}", "@backstage/plugin-github-actions": "^{{version '@backstage/plugin-github-actions'}}", "@backstage/plugin-org": "^{{version '@backstage/plugin-org'}}", + "@backstage/plugin-permission-react": "^{{version '@backstage/plugin-permission-react'}}", "@backstage/plugin-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}", "@backstage/plugin-search": "^{{version '@backstage/plugin-search'}}", "@backstage/plugin-tech-radar": "^{{version '@backstage/plugin-tech-radar'}}", diff --git a/packages/create-app/templates/default-app/packages/app/src/App.tsx b/packages/create-app/templates/default-app/packages/app/src/App.tsx index 78949b0755..9b6518620d 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.tsx @@ -29,6 +29,9 @@ import { Root } from './components/Root'; import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; import { createApp } from '@backstage/app-defaults'; import { FlatRoutes } from '@backstage/core-app-api'; +import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; +import { PermissionedRoute } from '@backstage/plugin-permission-react'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; const app = createApp({ apis, @@ -75,11 +78,16 @@ const routes = ( path="/tech-radar" element={} /> - } /> + } + /> }> {searchPage} } /> + } /> ); diff --git a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx index 7e98c7d3e6..90738c44d4 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx @@ -28,10 +28,7 @@ import { Settings as SidebarSettings, UserSettingsSignInAvatar, } from '@backstage/plugin-user-settings'; -import { - SidebarSearchModal, - SearchContextProvider, -} from '@backstage/plugin-search'; +import { SidebarSearchModal } from '@backstage/plugin-search'; import { Sidebar, sidebarConfig, @@ -84,9 +81,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( } to="/search"> - - - {' '} + }> diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 2288b475a9..5dd70286bc 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -14,7 +14,7 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "app": "file:../app", + "app": "link:../app", "@backstage/backend-common": "^{{version '@backstage/backend-common'}}", "@backstage/backend-tasks": "^{{version '@backstage/backend-tasks'}}", "@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}", @@ -40,7 +40,7 @@ "pg": "^8.3.0", {{/if}} {{#if dbTypeSqlite}} - "sqlite3": "^5.0.1", + "@vscode/sqlite3": "^5.0.7", {{/if}} "winston": "^3.2.1" }, diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index a170884538..8061f5fb98 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,58 @@ # @backstage/dev-utils +## 0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + - @backstage/app-defaults@0.1.7 + - @backstage/integration-react@0.1.21 + +## 0.2.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + - @backstage/app-defaults@0.1.7-next.0 + - @backstage/integration-react@0.1.21-next.0 + +## 0.2.20 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + - @backstage/integration-react@0.1.20 + - @backstage/core-app-api@0.5.2 + - @backstage/app-defaults@0.1.6 + - @backstage/test-utils@0.2.4 + +## 0.2.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + - @backstage/plugin-catalog-react@0.6.13-next.1 + - @backstage/core-app-api@0.5.2-next.0 + - @backstage/app-defaults@0.1.6-next.1 + - @backstage/test-utils@0.2.4-next.0 + +## 0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/app-defaults@0.1.6-next.0 + - @backstage/integration-react@0.1.20-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.2.19 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index bf29c742a9..9d55d17592 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.2.19", + "version": "0.2.21", "private": false, "publishConfig": { "access": "public", @@ -29,14 +29,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/app-defaults": "^0.1.5", - "@backstage/core-app-api": "^0.5.1", - "@backstage/core-components": "^0.8.6", + "@backstage/app-defaults": "^0.1.7", + "@backstage/core-app-api": "^0.5.2", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/catalog-model": "^0.9.10", - "@backstage/integration-react": "^0.1.19", - "@backstage/plugin-catalog-react": "^0.6.12", - "@backstage/test-utils": "^0.2.3", + "@backstage/integration-react": "^0.1.21", + "@backstage/plugin-catalog-react": "^0.6.14", + "@backstage/test-utils": "^0.2.4", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -55,7 +55,7 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 9142446cb0..bbf55fac17 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -31,7 +31,7 @@ "@types/puppeteer": "^5.4.4", "chalk": "^4.0.0", "commander": "^6.1.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "fs-extra": "9.1.0", "handlebars": "^4.7.3", "pgtools": "^0.3.0", diff --git a/packages/embedded-techdocs-app/app-config.dev.yaml b/packages/embedded-techdocs-app/app-config.dev.yaml deleted file mode 100644 index 02d68c940e..0000000000 --- a/packages/embedded-techdocs-app/app-config.dev.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# NOTE: This file is used for testing techdocs-cli locally - -app: - title: Techdocs Preview App - baseUrl: http://localhost:3000 - -backend: - baseUrl: http://localhost:7007 - -techdocs: - builder: 'external' - requestUrl: http://localhost:7007/api diff --git a/packages/errors/package.json b/packages/errors/package.json index 53a0ea93b8..f97adef8e1 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -31,11 +31,11 @@ }, "dependencies": { "@backstage/types": "^0.1.1", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "serialize-error": "^8.0.1" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2-next.0", "@types/jest": "^26.0.7" }, "files": [ diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index c7bb73285f..ec0457dc1e 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/integration-react +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.1.20 + +### Patch Changes + +- cee44ad289: Added the ability to collect users `oauth` token from the `RepoUrlPicker` for use in the template manifest +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.1.19 ### Patch Changes diff --git a/packages/integration-react/api-report.md b/packages/integration-react/api-report.md index 2db29631a3..5b0c9d6174 100644 --- a/packages/integration-react/api-report.md +++ b/packages/integration-react/api-report.md @@ -29,6 +29,7 @@ export class ScmAuth implements ScmAuthApi { ProfileInfoApi & BackstageIdentityApi & SessionApi; + bitbucket: OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi; } >; static forAuthApi( @@ -82,6 +83,12 @@ export const scmAuthApiRef: ApiRef; export interface ScmAuthTokenOptions extends AuthRequestOptions { additionalScope?: { repoWrite?: boolean; + customScopes?: { + github?: string[]; + azure?: string[]; + bitbucket?: string[]; + gitlab?: string[]; + }; }; url: string; } diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index c8aa4fd3b9..8413442b3d 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "0.1.19", + "version": "0.1.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,7 +22,7 @@ }, "dependencies": { "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/integration": "^0.7.2", "@backstage/theme": "^0.2.14", @@ -35,16 +35,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "msw": "^0.35.0", - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" }, "files": [ "dist" diff --git a/packages/integration-react/src/api/ScmAuth.test.ts b/packages/integration-react/src/api/ScmAuth.test.ts index 2d0ea9b97d..1ad391d8d2 100644 --- a/packages/integration-react/src/api/ScmAuth.test.ts +++ b/packages/integration-react/src/api/ScmAuth.test.ts @@ -141,6 +141,58 @@ describe('ScmAuth', () => { }); }); + it('should support additional provided scopes from the caller', async () => { + const mockAuthApi = { + getAccessToken: async (scopes: string[]) => { + return scopes.join(' '); + }, + }; + + const githubAuth = ScmAuth.forGithub(mockAuthApi); + await expect( + githubAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { + customScopes: { github: ['org:read', 'workflow:write'] }, + }, + }), + ).resolves.toMatchObject({ + token: 'repo read:org read:user org:read workflow:write', + }); + + const gitlabAuth = ScmAuth.forGitlab(mockAuthApi); + await expect( + gitlabAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { customScopes: { gitlab: ['write_repository'] } }, + }), + ).resolves.toMatchObject({ + token: 'read_user read_api read_repository write_repository', + }); + + const azureAuth = ScmAuth.forAzure(mockAuthApi); + await expect( + azureAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { customScopes: { azure: ['vso.org'] } }, + }), + ).resolves.toMatchObject({ + token: 'vso.build vso.code vso.graph vso.project vso.profile vso.org', + }); + + const bitbucketAuth = ScmAuth.forBitbucket(mockAuthApi); + await expect( + bitbucketAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { + customScopes: { bitbucket: ['snippet:write', 'issue:write'] }, + }, + }), + ).resolves.toMatchObject({ + token: 'account team pullrequest snippet issue snippet:write issue:write', + }); + }); + it('should handle host option', () => { const mockAuthApi = { getAccessToken: jest.fn(), @@ -186,7 +238,7 @@ describe('ScmAuth', () => { await expect( emptyMux.getCredentials({ url: 'http://example.com' }), ).rejects.toThrow( - "No authentication provider available for access to 'http://example.com'", + "No auth provider available for 'http://example.com', see https://backstage.io/link?scm-auth", ); const scmAuth = ScmAuth.merge( @@ -204,12 +256,12 @@ describe('ScmAuth', () => { await expect( scmAuth.getCredentials({ url: 'http://not.example.com' }), ).rejects.toThrow( - "No authentication provider available for access to 'http://not.example.com'", + "No auth provider available for 'http://not.example.com', see https://backstage.io/link?scm-auth", ); await expect( scmAuth.getCredentials({ url: 'http://example.com:8080' }), ).rejects.toThrow( - "No authentication provider available for access to 'http://example.com:8080'", + "No auth provider available for 'http://example.com:8080', see https://backstage.io/link?scm-auth", ); }); }); diff --git a/packages/integration-react/src/api/ScmAuth.ts b/packages/integration-react/src/api/ScmAuth.ts index be3216962d..b3dab4029e 100644 --- a/packages/integration-react/src/api/ScmAuth.ts +++ b/packages/integration-react/src/api/ScmAuth.ts @@ -15,6 +15,7 @@ */ import { + bitbucketAuthApiRef, createApiFactory, githubAuthApiRef, gitlabAuthApiRef, @@ -35,6 +36,9 @@ type ScopeMapping = { repoWrite: string[]; }; +// An enum of all supported providers +type ProviderName = 'generic' | 'github' | 'azure' | 'bitbucket' | 'gitlab'; + class ScmAuthMux implements ScmAuthApi { #providers: Array; @@ -49,7 +53,7 @@ class ScmAuthMux implements ScmAuthApi { const provider = this.#providers.find(p => p.isUrlSupported(url)); if (!provider) { throw new Error( - `No authentication provider available for access to '${options.url}'`, + `No auth provider available for '${options.url}', see https://backstage.io/link?scm-auth`, ); } @@ -93,12 +97,14 @@ export class ScmAuth implements ScmAuthApi { github: githubAuthApiRef, gitlab: gitlabAuthApiRef, azure: microsoftAuthApiRef, + bitbucket: bitbucketAuthApiRef, }, - factory: ({ github, gitlab, azure }) => + factory: ({ github, gitlab, azure, bitbucket }) => ScmAuth.merge( ScmAuth.forGithub(github), ScmAuth.forGitlab(gitlab), ScmAuth.forAzure(azure), + ScmAuth.forBitbucket(bitbucket), ), }); } @@ -116,7 +122,7 @@ export class ScmAuth implements ScmAuthApi { }; }, ): ScmAuth { - return new ScmAuth(authApi, options.host, options.scopeMapping); + return new ScmAuth('generic', authApi, options.host, options.scopeMapping); } /** @@ -139,7 +145,7 @@ export class ScmAuth implements ScmAuthApi { }, ): ScmAuth { const host = options?.host ?? 'github.com'; - return new ScmAuth(githubAuthApi, host, { + return new ScmAuth('github', githubAuthApi, host, { default: ['repo', 'read:org', 'read:user'], repoWrite: ['gist'], }); @@ -165,7 +171,7 @@ export class ScmAuth implements ScmAuthApi { }, ): ScmAuth { const host = options?.host ?? 'gitlab.com'; - return new ScmAuth(gitlabAuthApi, host, { + return new ScmAuth('gitlab', gitlabAuthApi, host, { default: ['read_user', 'read_api', 'read_repository'], repoWrite: ['write_repository', 'api'], }); @@ -191,7 +197,7 @@ export class ScmAuth implements ScmAuthApi { }, ): ScmAuth { const host = options?.host ?? 'dev.azure.com'; - return new ScmAuth(microsoftAuthApi, host, { + return new ScmAuth('azure', microsoftAuthApi, host, { default: [ 'vso.build', 'vso.code', @@ -223,7 +229,7 @@ export class ScmAuth implements ScmAuthApi { }, ): ScmAuth { const host = options?.host ?? 'bitbucket.org'; - return new ScmAuth(bitbucketAuthApi, host, { + return new ScmAuth('bitbucket', bitbucketAuthApi, host, { default: ['account', 'team', 'pullrequest', 'snippet', 'issue'], repoWrite: ['pullrequest:write', 'snippet:write', 'issue:write'], }); @@ -240,11 +246,18 @@ export class ScmAuth implements ScmAuthApi { #api: OAuthApi; #host: string; #scopeMapping: ScopeMapping; + #providerName: ProviderName; - private constructor(api: OAuthApi, host: string, scopeMapping: ScopeMapping) { + private constructor( + providerName: ProviderName, + api: OAuthApi, + host: string, + scopeMapping: ScopeMapping, + ) { this.#api = api; this.#host = host; this.#scopeMapping = scopeMapping; + this.#providerName = providerName; } /** @@ -254,6 +267,16 @@ export class ScmAuth implements ScmAuthApi { return url.host === this.#host; } + private getAdditionalScopesForProvider( + additionalScopes: ScmAuthTokenOptions['additionalScope'], + ): string[] { + if (!additionalScopes?.customScopes || this.#providerName === 'generic') { + return []; + } + + return additionalScopes.customScopes?.[this.#providerName] ?? []; + } + /** * Fetches credentials for the given resource. */ @@ -267,7 +290,17 @@ export class ScmAuth implements ScmAuthApi { scopes.push(...this.#scopeMapping.repoWrite); } - const token = await this.#api.getAccessToken(scopes, restOptions); + const additionalScopes = + this.getAdditionalScopesForProvider(additionalScope); + + if (additionalScopes.length) { + scopes.push(...additionalScopes); + } + + const uniqueScopes = [...new Set(scopes)]; + + const token = await this.#api.getAccessToken(uniqueScopes, restOptions); + return { token, headers: { diff --git a/packages/integration-react/src/api/ScmAuthApi.ts b/packages/integration-react/src/api/ScmAuthApi.ts index 44b007c4a2..be65842bc1 100644 --- a/packages/integration-react/src/api/ScmAuthApi.ts +++ b/packages/integration-react/src/api/ScmAuthApi.ts @@ -44,6 +44,16 @@ export interface ScmAuthTokenOptions extends AuthRequestOptions { * the ability to create things like issues and pull requests. */ repoWrite?: boolean; + /** + * Allow an arbitrary list of scopes provided from the user + * to request from the provider. + */ + customScopes?: { + github?: string[]; + azure?: string[]; + bitbucket?: string[]; + gitlab?: string[]; + }; }; } diff --git a/packages/integration/package.json b/packages/integration/package.json index 10400d791f..d6d0edd77a 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@backstage/config": "^0.1.13", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "git-url-parse": "^11.6.0", "@octokit/rest": "^18.5.3", "@octokit/auth-app": "^3.4.0", @@ -39,9 +39,9 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2-next.0", "@backstage/config-loader": "^0.9.3", - "@backstage/test-utils": "^0.2.3", + "@backstage/test-utils": "^0.2.4-next.0", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", "msw": "^0.35.0" diff --git a/packages/release-manifests/.eslintrc.js b/packages/release-manifests/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/release-manifests/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/release-manifests/CHANGELOG.md b/packages/release-manifests/CHANGELOG.md new file mode 100644 index 0000000000..61e6c45992 --- /dev/null +++ b/packages/release-manifests/CHANGELOG.md @@ -0,0 +1,8 @@ +# @backstage/release-manifests + +## 0.0.1 + +### Patch Changes + +- aeb5c69abb: Introduces a new package with utilities for fetching release manifests. + This package will primarily be used by the `@backstage/cli` package. diff --git a/packages/release-manifests/README.md b/packages/release-manifests/README.md new file mode 100644 index 0000000000..6111173f15 --- /dev/null +++ b/packages/release-manifests/README.md @@ -0,0 +1,3 @@ +# @backstage/release-manifests + +This package provides a mapping between a Backstage release and the packages included in that release. diff --git a/packages/release-manifests/api-report.md b/packages/release-manifests/api-report.md new file mode 100644 index 0000000000..1ffa35838b --- /dev/null +++ b/packages/release-manifests/api-report.md @@ -0,0 +1,34 @@ +## API Report File for "@backstage/release-manifests" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public +export function getManifestByReleaseLine( + options: GetManifestByReleaseLineOptions, +): Promise; + +// @public +export type GetManifestByReleaseLineOptions = { + releaseLine: string; +}; + +// @public +export function getManifestByVersion( + options: GetManifestByVersionOptions, +): Promise; + +// @public +export type GetManifestByVersionOptions = { + version: string; +}; + +// @public +export type ReleaseManifest = { + releaseVersion: string; + packages: { + name: string; + version: string; + }[]; +}; +``` diff --git a/packages/release-manifests/package.json b/packages/release-manifests/package.json new file mode 100644 index 0000000000..8ddbb73bca --- /dev/null +++ b/packages/release-manifests/package.json @@ -0,0 +1,44 @@ +{ + "name": "@backstage/release-manifests", + "description": "Helper library for receiving release manifests", + "version": "0.0.1", + "private": false, + "main": "src/index.ts", + "types": "src/index.ts", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/release-manifests" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "cross-fetch": "^3.1.5" + }, + "devDependencies": { + "@backstage/test-utils": "^0.2.3", + "msw": "^0.35.0", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32" + }, + "files": [ + "dist" + ] +} diff --git a/packages/release-manifests/src/index.ts b/packages/release-manifests/src/index.ts new file mode 100644 index 0000000000..f8ea22a3cb --- /dev/null +++ b/packages/release-manifests/src/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Contains mapping between Backstage release and package versions. + * + * @packageDocumentation + */ + +export { getManifestByVersion, getManifestByReleaseLine } from './manifest'; +export type { + ReleaseManifest, + GetManifestByReleaseLineOptions, + GetManifestByVersionOptions, +} from './manifest'; diff --git a/packages/release-manifests/src/manifest.test.ts b/packages/release-manifests/src/manifest.test.ts new file mode 100644 index 0000000000..70894e5431 --- /dev/null +++ b/packages/release-manifests/src/manifest.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { getManifestByReleaseLine, getManifestByVersion } from './manifest'; +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; + +describe('getManifestByVersion', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + it('should return a list of packages in a release', async () => { + worker.use( + rest.get('*/v1/releases/0.0.0/manifest.json', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + packages: [{ name: '@backstage/core', version: '1.2.3' }], + }), + ), + ), + rest.get('*/v1/releases/999.0.1/manifest.json', (_, res, ctx) => + res(ctx.status(404), ctx.json({})), + ), + ); + + const pkgs = await getManifestByVersion({ version: '0.0.0' }); + expect(pkgs.packages).toEqual([ + { + name: '@backstage/core', + version: '1.2.3', + }, + ]); + + await expect(getManifestByVersion({ version: '999.0.1' })).rejects.toThrow( + 'No release found for 999.0.1 version', + ); + }); +}); + +describe('getManifestByReleaseLine', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + it('should return a list of packages in a release', async () => { + worker.use( + rest.get('*/v1/tags/main/manifest.json', (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + packages: [{ name: '@backstage/core', version: '1.2.3' }], + }), + ), + ), + rest.get('*/v1/tags/foo/manifest.json', (_, res, ctx) => + res(ctx.status(404), ctx.json({})), + ), + ); + + const pkgs = await getManifestByReleaseLine({ releaseLine: 'main' }); + expect(pkgs.packages).toEqual([ + { + name: '@backstage/core', + version: '1.2.3', + }, + ]); + + await expect( + getManifestByReleaseLine({ releaseLine: 'foo' }), + ).rejects.toThrow("No 'foo' release line found"); + }); +}); diff --git a/packages/release-manifests/src/manifest.ts b/packages/release-manifests/src/manifest.ts new file mode 100644 index 0000000000..955cf7308c --- /dev/null +++ b/packages/release-manifests/src/manifest.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fetch from 'cross-fetch'; + +const VERSIONS_DOMAIN = 'https://versions.backstage.io'; + +/** + * Contains mapping between Backstage release and package versions. + * @public + */ +export type ReleaseManifest = { + releaseVersion: string; + packages: { name: string; version: string }[]; +}; + +/** + * Options for {@link getManifestByVersion}. + * @public + */ +export type GetManifestByVersionOptions = { + version: string; +}; + +/** + * Returns a release manifest based on supplied version. + * @public + */ +export async function getManifestByVersion( + options: GetManifestByVersionOptions, +): Promise { + const url = `${VERSIONS_DOMAIN}/v1/releases/${encodeURIComponent( + options.version, + )}/manifest.json`; + const response = await fetch(url); + if (response.status === 404) { + throw new Error(`No release found for ${options.version} version`); + } + if (response.status !== 200) { + throw new Error( + `Unexpected response status ${response.status} when fetching release from ${url}.`, + ); + } + return await response.json(); +} + +/** + * Options for {@link getManifestByReleaseLine}. + * @public + */ +export type GetManifestByReleaseLineOptions = { + releaseLine: string; +}; + +/** + * Returns a release manifest based on supplied release line. + * @public + */ +export async function getManifestByReleaseLine( + options: GetManifestByReleaseLineOptions, +): Promise { + const url = `${VERSIONS_DOMAIN}/v1/tags/${encodeURIComponent( + options.releaseLine, + )}/manifest.json`; + const response = await fetch(url); + if (response.status === 404) { + throw new Error(`No '${options.releaseLine}' release line found`); + } + if (response.status !== 200) { + throw new Error( + `Unexpected response status ${response.status} when fetching release from ${url}.`, + ); + } + return await response.json(); +} diff --git a/packages/search-common/package.json b/packages/search-common/package.json index 1023158754..10b03ab663 100644 --- a/packages/search-common/package.json +++ b/packages/search-common/package.json @@ -40,7 +40,7 @@ "@backstage/plugin-permission-common": "^0.4.0-next.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0" + "@backstage/cli": "^0.13.2-next.0" }, "jest": { "roots": [ diff --git a/packages/embedded-techdocs-app/.eslintrc.js b/packages/techdocs-cli-embedded-app/.eslintrc.js similarity index 100% rename from packages/embedded-techdocs-app/.eslintrc.js rename to packages/techdocs-cli-embedded-app/.eslintrc.js diff --git a/packages/embedded-techdocs-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md similarity index 65% rename from packages/embedded-techdocs-app/CHANGELOG.md rename to packages/techdocs-cli-embedded-app/CHANGELOG.md index d8039973e7..38726ce10e 100644 --- a/packages/embedded-techdocs-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,4 +1,67 @@ -# embedded-techdocs-app +# techdocs-cli-embedded-app + +## 0.2.63 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.13.2 + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog@0.7.12 + - @backstage/app-defaults@0.1.7 + - @backstage/integration-react@0.1.21 + - @backstage/plugin-techdocs@0.13.3 + +## 0.2.63-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.13.2-next.0 + - @backstage/core-components@0.8.8-next.0 + - @backstage/app-defaults@0.1.7-next.0 + - @backstage/integration-react@0.1.21-next.0 + - @backstage/plugin-catalog@0.7.12-next.0 + - @backstage/plugin-techdocs@0.13.3-next.0 + +## 0.2.62 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/cli@0.13.1 + - @backstage/integration-react@0.1.20 + - @backstage/core-app-api@0.5.2 + - @backstage/plugin-techdocs@0.13.2 + - @backstage/app-defaults@0.1.6 + - @backstage/test-utils@0.2.4 + - @backstage/plugin-catalog@0.7.11 + +## 0.2.62-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + - @backstage/cli@0.13.1-next.1 + - @backstage/core-app-api@0.5.2-next.0 + - @backstage/plugin-techdocs@0.13.2-next.1 + - @backstage/plugin-catalog@0.7.11-next.1 + - @backstage/app-defaults@0.1.6-next.1 + - @backstage/test-utils@0.2.4-next.0 + +## 0.2.62-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-techdocs@0.13.2-next.0 + - @backstage/cli@0.13.1-next.0 + - @backstage/app-defaults@0.1.6-next.0 + - @backstage/integration-react@0.1.20-next.0 + - @backstage/plugin-catalog@0.7.11-next.0 ## 0.2.61 diff --git a/packages/embedded-techdocs-app/app-config.yaml b/packages/techdocs-cli-embedded-app/app-config.yaml similarity index 100% rename from packages/embedded-techdocs-app/app-config.yaml rename to packages/techdocs-cli-embedded-app/app-config.yaml diff --git a/packages/embedded-techdocs-app/cypress.json b/packages/techdocs-cli-embedded-app/cypress.json similarity index 100% rename from packages/embedded-techdocs-app/cypress.json rename to packages/techdocs-cli-embedded-app/cypress.json diff --git a/packages/embedded-techdocs-app/cypress/.eslintrc.json b/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json similarity index 100% rename from packages/embedded-techdocs-app/cypress/.eslintrc.json rename to packages/techdocs-cli-embedded-app/cypress/.eslintrc.json diff --git a/packages/embedded-techdocs-app/cypress/integration/app.js b/packages/techdocs-cli-embedded-app/cypress/integration/app.js similarity index 100% rename from packages/embedded-techdocs-app/cypress/integration/app.js rename to packages/techdocs-cli-embedded-app/cypress/integration/app.js diff --git a/packages/embedded-techdocs-app/package.json b/packages/techdocs-cli-embedded-app/package.json similarity index 75% rename from packages/embedded-techdocs-app/package.json rename to packages/techdocs-cli-embedded-app/package.json index b9dab2d19e..975bfef43b 100644 --- a/packages/embedded-techdocs-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,20 +1,20 @@ { - "name": "embedded-techdocs-app", - "version": "0.2.61", + "name": "techdocs-cli-embedded-app", + "version": "0.2.63", "private": true, "bundled": true, "dependencies": { - "@backstage/app-defaults": "^0.1.5", + "@backstage/app-defaults": "^0.1.7", "@backstage/catalog-model": "^0.9.10", - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@backstage/config": "^0.1.13", - "@backstage/core-app-api": "^0.5.1", - "@backstage/core-components": "^0.8.6", + "@backstage/core-app-api": "^0.5.2", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/integration-react": "^0.1.19", - "@backstage/plugin-catalog": "^0.7.10", - "@backstage/plugin-techdocs": "^0.13.1", - "@backstage/test-utils": "^0.2.3", + "@backstage/integration-react": "^0.1.21", + "@backstage/plugin-catalog": "^0.7.12", + "@backstage/plugin-techdocs": "^0.13.3", + "@backstage/test-utils": "^0.2.4", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -26,7 +26,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -39,9 +39,8 @@ "start-server-and-test": "^1.10.11" }, "scripts": { - "start": "backstage-cli app:serve --config ./app-config.yaml --config ./app-config.dev.yaml", + "start": "backstage-cli app:serve --config ./app-config.yaml", "build": "backstage-cli app:build --config ./app-config.yaml", - "build:dev": "backstage-cli app:build --config ./app-config.dev.yaml", "clean": "backstage-cli clean", "test": "backstage-cli test", "lint": "backstage-cli lint", diff --git a/packages/embedded-techdocs-app/public/android-chrome-192x192.png b/packages/techdocs-cli-embedded-app/public/android-chrome-192x192.png similarity index 100% rename from packages/embedded-techdocs-app/public/android-chrome-192x192.png rename to packages/techdocs-cli-embedded-app/public/android-chrome-192x192.png diff --git a/packages/embedded-techdocs-app/public/apple-touch-icon.png b/packages/techdocs-cli-embedded-app/public/apple-touch-icon.png similarity index 100% rename from packages/embedded-techdocs-app/public/apple-touch-icon.png rename to packages/techdocs-cli-embedded-app/public/apple-touch-icon.png diff --git a/packages/embedded-techdocs-app/public/favicon-16x16.png b/packages/techdocs-cli-embedded-app/public/favicon-16x16.png similarity index 100% rename from packages/embedded-techdocs-app/public/favicon-16x16.png rename to packages/techdocs-cli-embedded-app/public/favicon-16x16.png diff --git a/packages/embedded-techdocs-app/public/favicon-32x32.png b/packages/techdocs-cli-embedded-app/public/favicon-32x32.png similarity index 100% rename from packages/embedded-techdocs-app/public/favicon-32x32.png rename to packages/techdocs-cli-embedded-app/public/favicon-32x32.png diff --git a/packages/embedded-techdocs-app/public/favicon.ico b/packages/techdocs-cli-embedded-app/public/favicon.ico similarity index 100% rename from packages/embedded-techdocs-app/public/favicon.ico rename to packages/techdocs-cli-embedded-app/public/favicon.ico diff --git a/packages/embedded-techdocs-app/public/index.html b/packages/techdocs-cli-embedded-app/public/index.html similarity index 100% rename from packages/embedded-techdocs-app/public/index.html rename to packages/techdocs-cli-embedded-app/public/index.html diff --git a/packages/embedded-techdocs-app/public/manifest.json b/packages/techdocs-cli-embedded-app/public/manifest.json similarity index 100% rename from packages/embedded-techdocs-app/public/manifest.json rename to packages/techdocs-cli-embedded-app/public/manifest.json diff --git a/packages/embedded-techdocs-app/public/robots.txt b/packages/techdocs-cli-embedded-app/public/robots.txt similarity index 100% rename from packages/embedded-techdocs-app/public/robots.txt rename to packages/techdocs-cli-embedded-app/public/robots.txt diff --git a/packages/embedded-techdocs-app/public/safari-pinned-tab.svg b/packages/techdocs-cli-embedded-app/public/safari-pinned-tab.svg similarity index 100% rename from packages/embedded-techdocs-app/public/safari-pinned-tab.svg rename to packages/techdocs-cli-embedded-app/public/safari-pinned-tab.svg diff --git a/packages/embedded-techdocs-app/src/App.test.tsx b/packages/techdocs-cli-embedded-app/src/App.test.tsx similarity index 67% rename from packages/embedded-techdocs-app/src/App.test.tsx rename to packages/techdocs-cli-embedded-app/src/App.test.tsx index f177433d95..75658d271e 100644 --- a/packages/embedded-techdocs-app/src/App.test.tsx +++ b/packages/techdocs-cli-embedded-app/src/App.test.tsx @@ -18,25 +18,24 @@ import React from 'react'; import { renderWithEffects } from '@backstage/test-utils'; import App from './App'; +jest.mock('./config', () => ({ + configLoader: async () => [ + { + data: { + app: { title: 'Test' }, + backend: { baseUrl: 'http://localhost:7007' }, + techdocs: { + storageUrl: 'http://localhost:7007/api/techdocs/static/docs', + }, + }, + context: 'test', + }, + ], +})); + describe('App', () => { it('should render', async () => { - process.env = { - NODE_ENV: 'test', - APP_CONFIG: [ - { - data: { - app: { title: 'Test' }, - backend: { baseUrl: 'http://localhost:7007' }, - techdocs: { - storageUrl: 'http://localhost:7007/api/techdocs/static/docs', - }, - }, - context: 'test', - }, - ] as any, - }; - const rendered = await renderWithEffects(); - expect(rendered.baseElement).toBeInTheDocument(); + expect(rendered.getByText('Docs Preview')).toBeInTheDocument(); }); }); diff --git a/packages/embedded-techdocs-app/src/App.tsx b/packages/techdocs-cli-embedded-app/src/App.tsx similarity index 97% rename from packages/embedded-techdocs-app/src/App.tsx rename to packages/techdocs-cli-embedded-app/src/App.tsx index 00b232d8e1..51bdfbbb11 100644 --- a/packages/embedded-techdocs-app/src/App.tsx +++ b/packages/techdocs-cli-embedded-app/src/App.tsx @@ -29,9 +29,11 @@ import { apis } from './apis'; import { Root } from './components/Root'; import { techDocsPage } from './components/TechDocsPage'; import * as plugins from './plugins'; +import { configLoader } from './config'; const app = createApp({ apis, + configLoader, plugins: Object.values(plugins), }); diff --git a/packages/embedded-techdocs-app/src/apis.ts b/packages/techdocs-cli-embedded-app/src/apis.ts similarity index 100% rename from packages/embedded-techdocs-app/src/apis.ts rename to packages/techdocs-cli-embedded-app/src/apis.ts diff --git a/packages/embedded-techdocs-app/src/components/Root/LogoFull.tsx b/packages/techdocs-cli-embedded-app/src/components/Root/LogoFull.tsx similarity index 100% rename from packages/embedded-techdocs-app/src/components/Root/LogoFull.tsx rename to packages/techdocs-cli-embedded-app/src/components/Root/LogoFull.tsx diff --git a/packages/embedded-techdocs-app/src/components/Root/LogoIcon.tsx b/packages/techdocs-cli-embedded-app/src/components/Root/LogoIcon.tsx similarity index 100% rename from packages/embedded-techdocs-app/src/components/Root/LogoIcon.tsx rename to packages/techdocs-cli-embedded-app/src/components/Root/LogoIcon.tsx diff --git a/packages/embedded-techdocs-app/src/components/Root/Root.tsx b/packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx similarity index 100% rename from packages/embedded-techdocs-app/src/components/Root/Root.tsx rename to packages/techdocs-cli-embedded-app/src/components/Root/Root.tsx diff --git a/packages/embedded-techdocs-app/src/components/Root/index.ts b/packages/techdocs-cli-embedded-app/src/components/Root/index.ts similarity index 100% rename from packages/embedded-techdocs-app/src/components/Root/index.ts rename to packages/techdocs-cli-embedded-app/src/components/Root/index.ts diff --git a/packages/embedded-techdocs-app/src/components/TechDocsPage/TechDocsPage.tsx b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx similarity index 100% rename from packages/embedded-techdocs-app/src/components/TechDocsPage/TechDocsPage.tsx rename to packages/techdocs-cli-embedded-app/src/components/TechDocsPage/TechDocsPage.tsx diff --git a/packages/embedded-techdocs-app/src/components/TechDocsPage/index.ts b/packages/techdocs-cli-embedded-app/src/components/TechDocsPage/index.ts similarity index 100% rename from packages/embedded-techdocs-app/src/components/TechDocsPage/index.ts rename to packages/techdocs-cli-embedded-app/src/components/TechDocsPage/index.ts diff --git a/packages/techdocs-cli-embedded-app/src/config.ts b/packages/techdocs-cli-embedded-app/src/config.ts new file mode 100644 index 0000000000..482ceb41ec --- /dev/null +++ b/packages/techdocs-cli-embedded-app/src/config.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defaultConfigLoader } from '@backstage/core-app-api'; + +const PRODUCTION_CONFIG = { + backend: { + baseUrl: 'http://localhost:3000', + }, + techdocs: { + builder: 'external', + requestUrl: 'http://localhost:3000/api', + }, +}; + +const DEVELOPMENT_CONFIG = { + backend: { + baseUrl: 'http://localhost:7007', + }, + techdocs: { + builder: 'external', + requestUrl: 'http://localhost:7007/api', + }, +}; + +async function isProductionServe() { + const res = await fetch('/.detect'); + if (!res.ok) { + return false; + } + const text = await res.text(); + return text.trim() === 'techdocs-cli-server'; +} + +export async function configLoader() { + const defaultConfigs = await defaultConfigLoader(); + const isProduction = await isProductionServe(); + + return [ + ...defaultConfigs, + { + context: 'detected', + data: isProduction ? PRODUCTION_CONFIG : DEVELOPMENT_CONFIG, + }, + ]; +} diff --git a/packages/embedded-techdocs-app/src/index.tsx b/packages/techdocs-cli-embedded-app/src/index.tsx similarity index 100% rename from packages/embedded-techdocs-app/src/index.tsx rename to packages/techdocs-cli-embedded-app/src/index.tsx diff --git a/packages/embedded-techdocs-app/src/plugins.ts b/packages/techdocs-cli-embedded-app/src/plugins.ts similarity index 100% rename from packages/embedded-techdocs-app/src/plugins.ts rename to packages/techdocs-cli-embedded-app/src/plugins.ts diff --git a/packages/embedded-techdocs-app/src/setupTests.ts b/packages/techdocs-cli-embedded-app/src/setupTests.ts similarity index 100% rename from packages/embedded-techdocs-app/src/setupTests.ts rename to packages/techdocs-cli-embedded-app/src/setupTests.ts diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index c261e83731..98015f766a 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,46 @@ # @techdocs/cli +## 0.8.13 + +### Patch Changes + +- b70c186194: Updated the HTTP server to allow for simplification of the development of the CLI itself. +- Updated dependencies + - @backstage/backend-common@0.10.7 + - @backstage/techdocs-common@0.11.7 + +## 0.8.13-next.0 + +### Patch Changes + +- b70c186194: Updated the HTTP server to allow for simplification of the development of the CLI itself. +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + - @backstage/techdocs-common@0.11.7-next.0 + +## 0.8.12 + +### Patch Changes + +- 14472509a3: Use a local file dependency for techdocs-cli-embedded-app, to ensure that it's always pulled out of the workspace +- Updated dependencies + - @backstage/backend-common@0.10.6 + - @backstage/techdocs-common@0.11.6 + +## 0.8.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + - @backstage/techdocs-common@0.11.6-next.0 + +## 0.8.12-next.0 + +### Patch Changes + +- 14472509a3: Use a local file dependency for techdocs-cli-embedded-app, to ensure that it's always pulled out of the workspace + ## 0.8.11 ### Patch Changes @@ -138,10 +179,10 @@ #### :house: Internal - - `embedded-techdocs-app` - - [#122](https://github.com/backstage/techdocs-cli/pull/122) chore(deps-dev): bump @types/node from 12.20.20 to 16.7.1 in /packages/embedded-techdocs-app ([@dependabot[bot]](https://github.com/apps/dependabot)) - - [#120](https://github.com/backstage/techdocs-cli/pull/120) chore(deps-dev): bump @types/react-dom from 16.9.14 to 17.0.9 in /packages/embedded-techdocs-app ([@dependabot[bot]](https://github.com/apps/dependabot)) - - [#119](https://github.com/backstage/techdocs-cli/pull/119) chore(deps-dev): bump @testing-library/user-event from 12.8.3 to 13.2.1 in /packages/embedded-techdocs-app ([@dependabot[bot]](https://github.com/apps/dependabot)) + - `techdocs-cli-embedded-app` + - [#122](https://github.com/backstage/techdocs-cli/pull/122) chore(deps-dev): bump @types/node from 12.20.20 to 16.7.1 in /packages/techdocs-cli-embedded-app ([@dependabot[bot]](https://github.com/apps/dependabot)) + - [#120](https://github.com/backstage/techdocs-cli/pull/120) chore(deps-dev): bump @types/react-dom from 16.9.14 to 17.0.9 in /packages/techdocs-cli-embedded-app ([@dependabot[bot]](https://github.com/apps/dependabot)) + - [#119](https://github.com/backstage/techdocs-cli/pull/119) chore(deps-dev): bump @testing-library/user-event from 12.8.3 to 13.2.1 in /packages/techdocs-cli-embedded-app ([@dependabot[bot]](https://github.com/apps/dependabot)) - [#118](https://github.com/backstage/techdocs-cli/pull/118) chore(deps-dev): bump @testing-library/react from 10.4.9 to 12.0.0 ([@dependabot[bot]](https://github.com/apps/dependabot)) - Other - [#117](https://github.com/backstage/techdocs-cli/pull/117) chore(deps): bump @backstage/plugin-catalog from 0.6.11 to 0.6.12 ([@dependabot[bot]](https://github.com/apps/dependabot)) diff --git a/packages/techdocs-cli/README.md b/packages/techdocs-cli/README.md index 6f070147eb..3ac423cb38 100644 --- a/packages/techdocs-cli/README.md +++ b/packages/techdocs-cli/README.md @@ -8,7 +8,7 @@ See [techdocs-cli usage docs](https://backstage.io/docs/features/techdocs/cli). ## Development -NOTE: When we build `techdocs-cli` it copies the output `embedded-techdocs-app` +NOTE: When we build `techdocs-cli` it copies the output `techdocs-cli-embedded-app` bundle into the `packages/techdocs-cli/dist` which is then published with the `@techdocs/cli` npm package. @@ -16,7 +16,7 @@ bundle into the `packages/techdocs-cli/dist` which is then published with the ```sh # From the root of this repository run -# NOTE: This will build the embedded-techdocs-app and copy the output into the cli dist directory +# NOTE: This will build the techdocs-cli-embedded-app and copy the output into the cli dist directory yarn build --scope @techdocs/cli # Now execute the binary @@ -26,14 +26,14 @@ packages/techdocs-cli/bin/techdocs-cli export PATH=/path/to/backstage/packages/techdocs-cli/bin:$PATH ``` -If you want to test live test changes to the `packages/embedded-techdocs-app` +If you want to test live test changes to the `packages/techdocs-cli-embedded-app` you can serve the app and run the CLI using the following commands: ```sh -# Open a shell to the embedded-techdocs-app directory -cd packages/embedded-techdocs-app +# Open a shell to the techdocs-cli-embedded-app directory +cd packages/techdocs-cli-embedded-app -# Run the embedded-techdocs-app using dev mode +# Run the techdocs-cli-embedded-app using dev mode yarn start # In another shell use the techdocs-cli from the root of this repo diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index c1e150b7e3..19514b63d2 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "0.8.11", + "version": "0.8.13", "private": false, "publishConfig": { "access": "public" @@ -21,18 +21,19 @@ "types": "", "scripts": { "start": "nodemon --", - "build": "./scripts/build.sh", + "build": "backstage-cli build --outputs cjs", "clean": "backstage-cli clean", "lint": "backstage-cli lint", "test": "backstage-cli test --testPathIgnorePatterns=src/e2e.test.ts", "test:e2e": "backstage-cli test src/e2e.test.ts", - "test:e2e:ci": "backstage-cli test --watchAll=false --ci src/e2e.test.ts" + "test:e2e:ci": "backstage-cli test --watchAll=false --ci src/e2e.test.ts", + "prepack": "./scripts/prepack.sh" }, "bin": { "techdocs-cli": "bin/techdocs-cli" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/commander": "^2.12.2", "@types/fs-extra": "^9.0.6", "@types/http-proxy": "^1.17.4", @@ -40,7 +41,7 @@ "@types/node": "^14.14.32", "@types/serve-handler": "^6.1.0", "@types/webpack-env": "^1.15.3", - "embedded-techdocs-app": "file:../embedded-techdocs-app", + "techdocs-cli-embedded-app": "link:../techdocs-cli-embedded-app", "find-process": "^1.4.5", "nodemon": "^2.0.2", "ts-node": "^10.0.0" @@ -55,10 +56,11 @@ "ext": "ts" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/catalog-model": "^0.9.10", + "@backstage/cli-common": "^0.1.6", "@backstage/config": "^0.1.13", - "@backstage/techdocs-common": "^0.11.5", + "@backstage/techdocs-common": "^0.11.7", "@types/dockerode": "^3.3.0", "commander": "^6.1.0", "dockerode": "^3.3.1", diff --git a/packages/techdocs-cli/scripts/build.sh b/packages/techdocs-cli/scripts/build.sh deleted file mode 100755 index 630fef42f0..0000000000 --- a/packages/techdocs-cli/scripts/build.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# 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. - -set -e - -SCRIPT_DIR=$(dirname $0) -TECHDOCS_CLI_DIR="$SCRIPT_DIR"/.. -TECHDOCS_CLI_EMBEDDED_APP_DIR="$TECHDOCS_CLI_DIR"/../embedded-techdocs-app - -compile_and_build_cli() { - echo "📄 Compiling..." - yarn workspace @techdocs/cli tsc > /dev/null - echo "📦️ Building..." - pushd $TECHDOCS_CLI_DIR > /dev/null - npx backstage-cli build --outputs cjs > /dev/null - popd > /dev/null -} - -build_and_embed_app() { - echo "🚚 Embedding app..." - if [ "$TECHDOCS_CLI_DEV_MODE" = "true" ] ; then - yarn workspace embedded-techdocs-app build:dev > /dev/null - else - yarn workspace embedded-techdocs-app build > /dev/null - fi - cp -r "$TECHDOCS_CLI_EMBEDDED_APP_DIR"/dist "$TECHDOCS_CLI_DIR"/dist/techdocs-preview-bundle > /dev/null -} - -compile_and_build_cli -build_and_embed_app -echo "🏁 Ready!" diff --git a/packages/techdocs-cli/scripts/prepack.sh b/packages/techdocs-cli/scripts/prepack.sh new file mode 100755 index 0000000000..6b769de37c --- /dev/null +++ b/packages/techdocs-cli/scripts/prepack.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# 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. + +set -e + +SCRIPT_DIR=$(dirname $0) +TECHDOCS_CLI_DIR="$SCRIPT_DIR"/.. +TECHDOCS_CLI_EMBEDDED_APP_DIR="$TECHDOCS_CLI_DIR"/../techdocs-cli-embedded-app + +echo "🚚 Copying embedded app into dist/embedded-app" +rm -rf "$TECHDOCS_CLI_DIR"/dist/embedded-app +cp -r "$TECHDOCS_CLI_EMBEDDED_APP_DIR"/dist "$TECHDOCS_CLI_DIR"/dist/embedded-app +echo "🏁 Ready!" diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index 4d71de3e52..a6a671c2df 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -17,11 +17,30 @@ import { Command } from 'commander'; import path from 'path'; import openBrowser from 'react-dev-utils/openBrowser'; +import { findPaths } from '@backstage/cli-common'; import HTTPServer from '../../lib/httpServer'; import { runMkdocsServer } from '../../lib/mkdocsServer'; import { LogFunc, waitForSignal } from '../../lib/run'; import { createLogger } from '../../lib/utility'; +function findPreviewBundlePath(): string { + try { + return path.join( + path.dirname(require.resolve('techdocs-cli-embedded-app/package.json')), + 'dist', + ); + } catch { + // If the techdocs-cli-embedded-app package is not available it means we're + // running a published package. For published packages the preview bundle is + // copied to dist/embedded-app be the prepack script. + // + // This can be tested by running `yarn pack` and extracting the resulting tarball into a directory. + // Within the extracted directory, run `npm install --only=prod`. + // Once that's done you can test the CLI in any directory using `node /package `. + return findPaths(__dirname).resolveOwn('dist/embedded-app'); + } +} + export default async function serve(cmd: Command) { const logger = createLogger({ verbose: cmd.verbose }); @@ -91,16 +110,9 @@ export default async function serve(cmd: Command) { ); } - // Run the embedded-techdocs Backstage app - const techdocsPreviewBundlePath = path.join( - path.dirname(require.resolve('@techdocs/cli/package.json')), - 'dist', - 'techdocs-preview-bundle', - ); - const port = isDevMode ? backstageBackendPort : backstagePort; const httpServer = new HTTPServer( - techdocsPreviewBundlePath, + findPreviewBundlePath(), port, cmd.mkdocsPort, cmd.verbose, diff --git a/packages/techdocs-cli/src/lib/httpServer.ts b/packages/techdocs-cli/src/lib/httpServer.ts index 0402ce4e69..92fc5abc87 100644 --- a/packages/techdocs-cli/src/lib/httpServer.ts +++ b/packages/techdocs-cli/src/lib/httpServer.ts @@ -71,10 +71,18 @@ export default class HTTPServer { response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); request.url = forwardPath; - return proxy.web(request, response); + proxy.web(request, response); + return; } - return serveHandler(request, response, { + // This endpoint is used by the frontend to detect where the backend is running. + if (request.url === '/.detect') { + response.setHeader('Content-Type', 'text/plain'); + response.end('techdocs-cli-server'); + return; + } + + serveHandler(request, response, { public: this.backstageBundleDir, trailingSlash: true, rewrites: [{ source: '**', destination: 'index.html' }], diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 9a3fa5051b..44fbec4171 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/techdocs-common +## 0.11.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + +## 0.11.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.11.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.11.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + ## 0.11.5 ### Patch Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index a82dda2eae..f45f842100 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.11.5", + "version": "0.11.7", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -38,7 +38,7 @@ "dependencies": { "@azure/identity": "^2.0.1", "@azure/storage-blob": "^12.5.0", - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", @@ -60,7 +60,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", diff --git a/packages/techdocs-common/src/helpers.ts b/packages/techdocs-common/src/helpers.ts index 7a9e4b18be..ccf1256381 100644 --- a/packages/techdocs-common/src/helpers.ts +++ b/packages/techdocs-common/src/helpers.ts @@ -18,7 +18,7 @@ import { resolveSafeChildPath, UrlReader } from '@backstage/backend-common'; import { Entity, getEntitySourceLocation, - parseLocationReference, + parseLocationRef, } from '@backstage/catalog-model'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -42,7 +42,7 @@ export const parseReferenceAnnotation = ( ); } - const { type, target } = parseLocationReference(annotation); + const { type, target } = parseLocationRef(annotation); return { type: type as RemoteProtocol, target, diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 1867b52265..35c9ebfb35 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/test-utils +## 0.2.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.5.2 + +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.5.2-next.0 + ## 0.2.3 ### Patch Changes diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index e396a47917..67b68e08cc 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.2.3", + "version": "0.2.4", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/config": "^0.1.13", - "@backstage/core-app-api": "^0.5.1", + "@backstage/core-app-api": "^0.5.2", "@backstage/core-plugin-api": "^0.6.0", "@backstage/plugin-permission-common": "^0.4.0", "@backstage/plugin-permission-react": "^0.3.0", @@ -41,7 +41,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "zen-observable": "^0.8.15" @@ -51,7 +51,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2-next.0", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "msw": "^0.35.0" diff --git a/packages/theme/package.json b/packages/theme/package.json index cd7517b2d8..2846db2805 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -31,7 +31,7 @@ "@material-ui/core": "^4.12.2" }, "devDependencies": { - "@backstage/cli": "^0.13.0" + "@backstage/cli": "^0.13.2-next.0" }, "files": [ "dist" diff --git a/packages/types/package.json b/packages/types/package.json index 881e30081f..ae18fe519d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -31,7 +31,7 @@ }, "dependencies": {}, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2-next.0", "@types/zen-observable": "^0.8.0", "zen-observable": "^0.8.15" }, diff --git a/packages/version-bridge/package.json b/packages/version-bridge/package.json index ce5456bea8..6989aec3cf 100644 --- a/packages/version-bridge/package.json +++ b/packages/version-bridge/package.json @@ -33,7 +33,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2" diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md index 36d322a0e0..df784d0030 100644 --- a/plugins/airbrake/CHANGELOG.md +++ b/plugins/airbrake/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-airbrake +## 0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.1.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.1.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.1.1 ### Patch Changes diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json index e037e38009..40b4787dd5 100644 --- a/plugins/airbrake/package.json +++ b/plugins/airbrake/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-airbrake", - "version": "0.1.1", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -34,18 +34,18 @@ }, "devDependencies": { "@types/object-hash": "^2.2.1", - "@backstage/app-defaults": "^0.1.5", - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/app-defaults": "^0.1.7", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "msw": "^0.35.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "react-router": "6.0.0-beta.0" }, "files": [ diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index 290838ac3d..9f5d2cc730 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-allure +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.1.12 ### Patch Changes diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 4d8768dc03..5ea3129746 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-allure", "description": "A Backstage plugin that integrates with Allure", - "version": "0.1.12", + "version": "0.1.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,9 +23,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -37,16 +37,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md index 8b82c050d3..e24ea89a0a 100644 --- a/plugins/analytics-module-ga/CHANGELOG.md +++ b/plugins/analytics-module-ga/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-analytics-module-ga +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.1.8 + +### Patch Changes + +- b40a0ccc4d: Added the ability to capture and set user IDs from Backstage's `identityApi`. For full instructions on how to + set this up, see [the User ID section of its README](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-ga#user-ids) +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.1.8-next.0 + +### Patch Changes + +- b40a0ccc4d: Added the ability to capture and set user IDs from Backstage's `identityApi`. For full instructions on how to + set this up, see [the User ID section of its README](https://github.com/backstage/backstage/tree/master/plugins/analytics-module-ga#user-ids) +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.1.7 ### Patch Changes diff --git a/plugins/analytics-module-ga/README.md b/plugins/analytics-module-ga/README.md index a0c4fe517a..92cfd7e9ae 100644 --- a/plugins/analytics-module-ga/README.md +++ b/plugins/analytics-module-ga/README.md @@ -14,15 +14,22 @@ This plugin contains no other functionality. ```tsx // packages/app/src/apis.ts -import { analyticsApiRef, configApiRef } from '@backstage/core-plugin-api'; +import { + analyticsApiRef, + configApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; import { GoogleAnalytics } from '@backstage/plugin-analytics-module-ga'; export const apis: AnyApiFactory[] = [ // Instantiate and register the GA Analytics API Implementation. createApiFactory({ api: analyticsApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => GoogleAnalytics.fromConfig(configApi), + deps: { configApi: configApiRef, identityApi: identityApiRef }, + factory: ({ configApi, identityApi }) => + GoogleAnalytics.fromConfig(configApi, { + identityApi, + }), }), ]; ``` @@ -92,6 +99,63 @@ app: key: someEventContextAttr ``` +### User IDs + +This plugin supports accurately deriving user-oriented metrics (like monthly +active users) using Google Analytics' [user ID views][ga-user-id-view]. To +enable this... + +1. Be sure you've gone through the process of setting up a user ID view in your + Backstage instance's Google Analytics property (see docs linked above). +2. Make sure you instantiate `GoogleAnalytics` with an `identityApi` instance + passed to it, as shown in the installation section above. +3. Set `app.analytics.ga.identity` to either `required` or `optional` in your + `app.config.yaml`, like this: + + ```yaml + app: + analytics: + ga: + trackingId: UA-0000000-0 + identity: optional + ``` + + Set `identity` to `optional` if you need accurate session counts, including + cases where users do not sign in at all. Use `required` if you need all hits + to be associated with a user ID without exception (and don't mind if some + sessions are not captured, such as those where no sign in occur). + +Note that, to comply with GA policies, the value of the User ID is +pseudonymized before being sent to GA. By default, it is a `sha256` hash of the +current user's `userEntityRef` as returned by the `identityApi`. To set a +different value, provide a `userIdTransform` function alongside `identityApi` +when you instantiate `GoogleAnalytics`. This function will be passed the +`userEntityRef` as an argument and should resolve to the value you wish to set +as the user ID. For example: + +```typescript +import { + analyticsApiRef, + configApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { GoogleAnalytics } from '@backstage/plugin-analytics-module-ga'; + +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: analyticsApiRef, + deps: { configApi: configApiRef, identityApi: identityApiRef }, + factory: ({ configApi, identityApi }) => + GoogleAnalytics.fromConfig(configApi, { + identityApi, + userIdTransform: async (userEntityRef: string): Promise => { + return customHashingFunction(userEntityRef); + }, + }), + }), +]; +``` + ### Debugging and Testing In pre-production environments, you may wish to set additional configurations @@ -147,3 +211,4 @@ app: [what-is-a-custom-dimension]: https://support.google.com/analytics/answer/2709828 [configure-custom-dimension]: https://support.google.com/analytics/answer/2709828#configuration +[ga-user-id-view]: https://support.google.com/analytics/answer/3123669 diff --git a/plugins/analytics-module-ga/api-report.md b/plugins/analytics-module-ga/api-report.md index b0f3b736c3..9a359f81e9 100644 --- a/plugins/analytics-module-ga/api-report.md +++ b/plugins/analytics-module-ga/api-report.md @@ -7,18 +7,23 @@ import { AnalyticsApi } from '@backstage/core-plugin-api'; import { AnalyticsEvent } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Config } from '@backstage/config'; +import { IdentityApi } from '@backstage/core-plugin-api'; -// Warning: (ae-missing-release-tag) "analyticsModuleGA" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public @deprecated (undocumented) export const analyticsModuleGA: BackstagePlugin<{}, {}>; -// Warning: (ae-missing-release-tag) "GoogleAnalytics" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export class GoogleAnalytics implements AnalyticsApi { captureEvent(event: AnalyticsEvent): void; - static fromConfig(config: Config): GoogleAnalytics; + static fromConfig( + config: Config, + options?: { + identityApi?: IdentityApi; + userIdTransform?: + | 'sha-256' + | ((userEntityRef: string) => Promise); + }, + ): GoogleAnalytics; } // (No @packageDocumentation comment for this package) diff --git a/plugins/analytics-module-ga/config.d.ts b/plugins/analytics-module-ga/config.d.ts index ac534daae8..b91f5812c1 100644 --- a/plugins/analytics-module-ga/config.d.ts +++ b/plugins/analytics-module-ga/config.d.ts @@ -34,6 +34,25 @@ export interface Config { */ scriptSrc?: string; + /** + * Controls how the identityApi is used when sending data to GA: + * + * - `disabled`: (Default) Explicitly prevents a user's identity from + * being used when capturing events in GA. + * - `optional`: Pageviews and hits are forwarded to GA as they happen + * and only include user identity metadata once known. Guarantees + * that hits are captured for all sessions, even if no sign in + * occurs, but may result in dropped hits in User ID views. + * - `required`: All pageviews and hits are deferred until an identity + * is known. Guarantees that all data sent to GA correlates to a user + * identity, but prevents GA from receiving events for sessions in + * which a user does not sign in. An `identityApi` instance must be + * passed during instantiation when set to this value. + * + * @visibility frontend + */ + identity?: 'disabled' | 'optional' | 'required'; + /** * Whether or not to log analytics debug statements to the console. * Defaults to false. diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 255a0cae8e..0b0c3a6cdc 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-analytics-module-ga", - "version": "0.1.7", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,7 +22,7 @@ }, "dependencies": { "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -35,16 +35,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts index cbcf95cb55..a4f7195fec 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts @@ -15,10 +15,17 @@ */ import { ConfigReader } from '@backstage/config'; +import { IdentityApi } from '@backstage/core-plugin-api'; import ReactGA from 'react-ga'; import { GoogleAnalytics } from './GoogleAnalytics'; describe('GoogleAnalytics', () => { + const context = { + extension: 'App', + pluginId: 'some-plugin', + routeRef: 'unknown', + releaseNum: 1337, + }; const trackingId = 'UA-000000-0'; const basicValidConfig = new ConfigReader({ app: { analytics: { ga: { trackingId, testMode: true } } }, @@ -50,12 +57,6 @@ describe('GoogleAnalytics', () => { }); describe('integration', () => { - const context = { - extension: 'App', - pluginId: 'some-plugin', - routeRef: 'unknown', - releaseNum: 1337, - }; const advancedConfig = new ConfigReader({ app: { analytics: { @@ -141,20 +142,13 @@ describe('GoogleAnalytics', () => { context, }); - // Expect a set command first. - const [setCommand, setData] = ReactGA.testModeAPI.calls[1]; - expect(setCommand).toBe('set'); - expect(setData).toMatchObject({ - dimension1: context.pluginId, - metric1: context.releaseNum, - }); - - // Followed by a send command. - const [sendCommand, sendData] = ReactGA.testModeAPI.calls[2]; - expect(sendCommand).toBe('send'); - expect(sendData).toMatchObject({ + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ hitType: 'pageview', page: '/a-page', + dimension1: context.pluginId, + metric1: context.releaseNum, }); }); @@ -208,4 +202,214 @@ describe('GoogleAnalytics', () => { }); }); }); + + describe('identityApi', () => { + const identityApi = { + getBackstageIdentity: jest.fn().mockResolvedValue({ + userEntityRef: 'User:default/someone', + }), + } as unknown as IdentityApi; + + it('does not set userId unless explicitly configured', async () => { + // Instantiate with identityApi and default configs. + const api = GoogleAnalytics.fromConfig(basicValidConfig, { identityApi }); + api.captureEvent({ + action: 'navigate', + subject: '/', + context, + }); + + // Wait for any/all promises involved to settle. + await new Promise(resolve => setImmediate(resolve)); + + // A pageview should have been fired immediately. + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'pageview', + page: '/', + }); + + // There should not have been a UserID set. + expect(ReactGA.testModeAPI.calls).toHaveLength(2); + }); + + it('sets hashed userId when identityApi is provided', async () => { + // Instantiate with identityApi and identity set to optional + const optionalConfig = new ConfigReader({ + app: { + analytics: { + ga: { trackingId, testMode: true, identity: 'optional' }, + }, + }, + }); + const api = GoogleAnalytics.fromConfig(optionalConfig, { identityApi }); + api.captureEvent({ + action: 'navigate', + subject: '/', + context, + }); + + // Wait for any/all promises involved to settle. + await new Promise(resolve => setImmediate(resolve)); + + // A pageview should have been fired immediately. + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'pageview', + page: '/', + }); + + // User ID should have been set after the pageview. + const [setCommand, setData] = ReactGA.testModeAPI.calls[2]; + expect(setCommand).toBe('set'); + expect(setData).toMatchObject({ + // String indicating userEntityRef went through expected hashing. + userId: '557365723a64656661756c742f736f6d656f6e65', + }); + }); + + it('set custom-hashed userId when userIdTransform is provided', async () => { + const userIdTransform = jest.fn().mockResolvedValue('s0m3hash3dvalu3'); + const optionalConfig = new ConfigReader({ + app: { + analytics: { + ga: { trackingId, testMode: true, identity: 'optional' }, + }, + }, + }); + const api = GoogleAnalytics.fromConfig(optionalConfig, { + identityApi, + userIdTransform, + }); + api.captureEvent({ + action: 'navigate', + subject: '/', + context, + }); + + // Wait for any/all promises involved to settle. + await new Promise(resolve => setImmediate(resolve)); + + // User ID should have been set after the pageview. + const [setCommand, setData] = ReactGA.testModeAPI.calls[2]; + expect(setCommand).toBe('set'); + expect(setData).toMatchObject({ + userId: 's0m3hash3dvalu3', + }); + expect(userIdTransform).toHaveBeenCalledWith('User:default/someone'); + }); + + it('does not set userId when identityApi is provided and ga.identity is explicitly disabled', async () => { + // Instantiate with identityApi and identity explicitly disabled. + const disabledConfig = new ConfigReader({ + app: { + analytics: { + ga: { trackingId, testMode: true, identity: 'disabled' }, + }, + }, + }); + const api = GoogleAnalytics.fromConfig(disabledConfig, { identityApi }); + api.captureEvent({ + action: 'navigate', + subject: '/', + context, + }); + + // Wait for any/all promises involved to settle. + await new Promise(resolve => setImmediate(resolve)); + + // A pageview should have been fired immediately. + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'pageview', + page: '/', + }); + + // There should not have been a UserID set. + expect(ReactGA.testModeAPI.calls).toHaveLength(2); + }); + + it('throws error when ga.identity is required but no identityApi is provided', async () => { + // Instantiate without identityApi and identity explicitly disabled. + const requiredConfig = new ConfigReader({ + app: { + analytics: { + ga: { trackingId, testMode: true, identity: 'required' }, + }, + }, + }); + + expect(() => GoogleAnalytics.fromConfig(requiredConfig)).toThrow(); + }); + + it('defers event capture when ga.identity is required', async () => { + // Instantiate with identityApi and identity explicitly required. + const requiredConfig = new ConfigReader({ + app: { + analytics: { + ga: { trackingId, testMode: true, identity: 'required' }, + }, + }, + }); + const api = GoogleAnalytics.fromConfig(requiredConfig, { identityApi }); + + // Fire a pageview and an event. + api.captureEvent({ + action: 'navigate', + subject: '/', + context, + }); + api.captureEvent({ + action: 'test', + subject: 'some label', + context, + }); + + // Wait for any/all promises involved to settle. + await new Promise(resolve => setImmediate(resolve)); + + // User ID should have been set first. + const [setCommand, setData] = ReactGA.testModeAPI.calls[1]; + expect(setCommand).toBe('set'); + expect(setData).toMatchObject({ + // String indicating userEntityRef went through expected hashing. + userId: '557365723a64656661756c742f736f6d656f6e65', + }); + + // Then a pageview should have been fired with a queue time. + const [pageCommand, pageData] = ReactGA.testModeAPI.calls[2]; + expect(pageCommand).toBe('send'); + expect(pageData).toMatchObject({ + hitType: 'pageview', + page: '/', + queueTime: expect.any(Number), + }); + + // Then an event should have been fired with a queue time. + const [eventCommand, eventData] = ReactGA.testModeAPI.calls[3]; + expect(eventCommand).toBe('send'); + expect(eventData).toMatchObject({ + hitType: 'event', + queueTime: expect.any(Number), + }); + + // And subsequent hits should not have a queue time. + api.captureEvent({ + action: 'navigate', + subject: '/page-2', + context, + }); + + const [lastCommand, lastData] = ReactGA.testModeAPI.calls[4]; + expect(lastCommand).toBe('send'); + expect(lastData).toMatchObject({ + hitType: 'pageview', + page: '/page-2', + }); + expect(lastData.queueTime).toBeUndefined(); + }); + }); }); diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts index 28202607be..ad7205983c 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts @@ -20,8 +20,10 @@ import { AnalyticsContextValue, AnalyticsEventAttributes, AnalyticsEvent, + IdentityApi, } from '@backstage/core-plugin-api'; import { Config } from '@backstage/config'; +import { DeferredCapture } from '../../../util'; type CustomDimensionOrMetricConfig = { type: 'dimension' | 'metric'; @@ -32,21 +34,36 @@ type CustomDimensionOrMetricConfig = { /** * Google Analytics API provider for the Backstage Analytics API. + * @public */ export class GoogleAnalytics implements AnalyticsApi { private readonly cdmConfig: CustomDimensionOrMetricConfig[]; + private customUserIdTransform?: (userEntityRef: string) => Promise; + private readonly capture: DeferredCapture; /** * Instantiate the implementation and initialize ReactGA. */ private constructor(options: { + identityApi?: IdentityApi; + userIdTransform?: 'sha-256' | ((userEntityRef: string) => Promise); cdmConfig: CustomDimensionOrMetricConfig[]; + identity: string; trackingId: string; scriptSrc?: string; testMode: boolean; debug: boolean; }) { - const { cdmConfig, trackingId, scriptSrc, testMode, debug } = options; + const { + cdmConfig, + identity, + trackingId, + identityApi, + userIdTransform = 'sha-256', + scriptSrc, + testMode, + debug, + } = options; this.cdmConfig = cdmConfig; @@ -57,15 +74,37 @@ export class GoogleAnalytics implements AnalyticsApi { gaAddress: scriptSrc, titleCase: false, }); + + // If identity is required, defer event capture until identity is known. + this.capture = new DeferredCapture({ defer: identity === 'required' }); + + // Allow custom userId transformation. + this.customUserIdTransform = + typeof userIdTransform === 'function' ? userIdTransform : undefined; + + // Capture user only when explicitly enabled and provided. + if (identity !== 'disabled' && identityApi) { + this.setUserFrom(identityApi); + } } /** * Instantiate a fully configured GA Analytics API implementation. */ - static fromConfig(config: Config) { + static fromConfig( + config: Config, + options: { + identityApi?: IdentityApi; + userIdTransform?: + | 'sha-256' + | ((userEntityRef: string) => Promise); + } = {}, + ) { // Get all necessary configuration. const trackingId = config.getString('app.analytics.ga.trackingId'); const scriptSrc = config.getOptionalString('app.analytics.ga.scriptSrc'); + const identity = + config.getOptionalString('app.analytics.ga.identity') || 'disabled'; const debug = config.getOptionalBoolean('app.analytics.ga.debug') ?? false; const testMode = config.getOptionalBoolean('app.analytics.ga.testMode') ?? false; @@ -83,8 +122,16 @@ export class GoogleAnalytics implements AnalyticsApi { }; }) ?? []; + if (identity === 'required' && !options.identityApi) { + throw new Error( + 'Invalid config: identity API must be provided to deps when ga.identity is required', + ); + } + // Return an implementation instance. return new GoogleAnalytics({ + ...options, + identity, trackingId, scriptSrc, cdmConfig, @@ -103,16 +150,11 @@ export class GoogleAnalytics implements AnalyticsApi { const customMetadata = this.getCustomDimensionMetrics(context, attributes); if (action === 'navigate' && context.extension === 'App') { - // Set any/all custom dimensions. - if (Object.keys(customMetadata).length) { - ReactGA.set(customMetadata); - } - - ReactGA.pageview(subject); + this.capture.pageview(subject, customMetadata); return; } - ReactGA.event({ + this.capture.event({ category: context.extension || 'App', action, label: subject, @@ -151,4 +193,59 @@ export class GoogleAnalytics implements AnalyticsApi { return customDimensionsMetrics; } + + /** + * Sets the GA userId, based on the `userEntityRef` set on the backstage + * identity loaded from a given Backstage Identity API instance. Because + * Google forbids sending any PII (including on the userId field), we hash + * the entire `userEntityRef` on behalf of integrators: + * + * - With value `User:default/name`, userId becomes `sha256(User:default/name)` + * + * If an integrator wishes to use an alternative hashing mechanism or an + * entirely different value, they may do so by passing a `userIdTransform` + * function alongside the `identityApi` to `GoogleAnalytics.fromConfig()`. + * This function receives the `userEntityRef` as an argument and should + * resolve to a hashed version of whatever identifier they choose. + * + * Note: this feature requires that an integrator has set up a Google + * Analytics User ID view in the property used to track Backstage. + */ + private async setUserFrom(identityApi: IdentityApi) { + const { userEntityRef } = await identityApi.getBackstageIdentity(); + + // Prevent PII from being passed to Google Analytics. + const userId = await this.getPrivateUserId(userEntityRef); + + // Set the user ID. + ReactGA.set({ userId }); + + // Notify the deferred capture mechanism that it may proceed. + this.capture.setReady(); + } + + /** + * Returns a PII-free (according to Google's terms of service) user ID for + * use in Google Analytics. + */ + private getPrivateUserId(userEntityRef: string): Promise { + // Allow integrators to provide their own hashing transformer. + if (this.customUserIdTransform) { + return this.customUserIdTransform(userEntityRef); + } + + return this.hash(userEntityRef); + } + + /** + * Simple hash function; relies on web cryptography + the sha-256 algorithm. + */ + private async hash(value: string): Promise { + const digest = await crypto.subtle.digest( + 'sha-256', + new TextEncoder().encode(value), + ); + const hashArray = Array.from(new Uint8Array(digest)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + } } diff --git a/plugins/analytics-module-ga/src/index.ts b/plugins/analytics-module-ga/src/index.ts index 17a3213c6d..fca8d91658 100644 --- a/plugins/analytics-module-ga/src/index.ts +++ b/plugins/analytics-module-ga/src/index.ts @@ -15,4 +15,4 @@ */ export { analyticsModuleGA } from './plugin'; -export { GoogleAnalytics } from './apis/implementations/AnalyticsApi'; +export * from './apis/implementations/AnalyticsApi'; diff --git a/plugins/analytics-module-ga/src/plugin.ts b/plugins/analytics-module-ga/src/plugin.ts index d63ae6616f..4eec63aa2e 100644 --- a/plugins/analytics-module-ga/src/plugin.ts +++ b/plugins/analytics-module-ga/src/plugin.ts @@ -15,6 +15,12 @@ */ import { createPlugin } from '@backstage/core-plugin-api'; +/** + * @deprecated Importing and including this plugin in an app has no effect. + * This will be removed in a future release. + * + * @public + */ export const analyticsModuleGA = createPlugin({ id: 'analytics-provider-ga', }); diff --git a/plugins/analytics-module-ga/src/setupTests.ts b/plugins/analytics-module-ga/src/setupTests.ts index fc6dbd98f8..4ed20ac097 100644 --- a/plugins/analytics-module-ga/src/setupTests.ts +++ b/plugins/analytics-module-ga/src/setupTests.ts @@ -15,3 +15,20 @@ */ import '@testing-library/jest-dom'; import 'cross-fetch/polyfill'; + +// eslint-disable-next-line no-restricted-imports +import { TextEncoder } from 'util'; + +// Mock browser crypto.subtle.digest method for sha-256 hashing. +Object.defineProperty(global.self, 'crypto', { + value: { + subtle: { + digest: (_algo: string, data: Uint8Array): ArrayBuffer => data.buffer, + }, + }, +}); + +// Also used in browser-based APIs for hashing. +Object.defineProperty(global.self, 'TextEncoder', { + value: TextEncoder, +}); diff --git a/plugins/analytics-module-ga/src/util/DeferredCapture.ts b/plugins/analytics-module-ga/src/util/DeferredCapture.ts new file mode 100644 index 0000000000..38afecd74d --- /dev/null +++ b/plugins/analytics-module-ga/src/util/DeferredCapture.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import ReactGA from 'react-ga'; + +type Hit = { + timestamp: number; + data: { + hitType: 'pageview' | 'event'; + [x: string]: any; + }; +}; + +/** + * A wrapper around ReactGA that can optionally handle latent capture logic. + * + * - When defer is `false`, event data is sent directly to GA. + * - When defer is `true`, event data is queued (with a timestamp), so that it + * can be sent to GA once externally indicated to be ready. This relies on + * the `qt` or `queueTime` parameter of the Measurement Protocol. + * + * @see https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#qt + */ +export class DeferredCapture { + /** + * Queue of deferred hits to be processed when ready. When undefined, hits + * can safely be sent without delay. + */ + private queue: Hit[] | undefined; + + constructor({ defer = false }: { defer: boolean }) { + this.queue = defer ? [] : undefined; + } + + /** + * Indicates that deferred capture may now proceed. + */ + setReady() { + if (this.queue) { + this.queue.forEach(this.sendDeferred); + this.queue = undefined; + } + } + + /** + * Either forwards the pageview directly to GA, or (if configured) enqueues + * the pageview hit to be captured when ready. + */ + pageview(path: string, metadata: ReactGA.FieldsObject = {}) { + if (this.queue) { + this.queue.push({ + timestamp: Date.now(), + data: { + hitType: 'pageview', + page: path, + ...metadata, + }, + }); + return; + } + + ReactGA.send({ + hitType: 'pageview', + page: path, + ...metadata, + }); + } + + /** + * Either forwards the event directly to GA, or (if configured) enqueues the + * event hit to be captured when ready. + */ + event(eventDetails: ReactGA.EventArgs) { + if (this.queue) { + this.queue.push({ + timestamp: Date.now(), + data: { + ...eventDetails, + hitType: 'event', + }, + }); + return; + } + + ReactGA.event(eventDetails); + } + + /** + * Sends a given hit to GA, decorated with the correct queue time. + */ + private sendDeferred(hit: Hit) { + // Send the hit with the appropriate queue time (`qt`). + ReactGA.send({ + ...hit.data, + queueTime: Date.now() - hit.timestamp, + }); + } +} diff --git a/plugins/analytics-module-ga/src/util/index.ts b/plugins/analytics-module-ga/src/util/index.ts new file mode 100644 index 0000000000..3b7a90c808 --- /dev/null +++ b/plugins/analytics-module-ga/src/util/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { DeferredCapture } from './DeferredCapture'; diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md index 4349718587..03bef4543a 100644 --- a/plugins/apache-airflow/CHANGELOG.md +++ b/plugins/apache-airflow/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-apache-airflow +## 0.1.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.1.4 ### Patch Changes diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index 32fdd8b5cb..e36e925fcb 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.1.4", + "version": "0.1.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,12 +20,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "qs": "^6.10.1", "react-use": "^17.2.4" }, @@ -33,10 +33,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 5e495a99a8..73de279056 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-api-docs +## 0.7.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + - @backstage/plugin-catalog@0.7.12 + +## 0.7.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + - @backstage/plugin-catalog@0.7.12-next.0 + +## 0.7.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + - @backstage/plugin-catalog@0.7.11 + +## 0.7.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog@0.7.11-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.7.0 ### Minor Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 08a1de6464..3b92cdf17e 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.7.0", + "version": "0.7.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "dependencies": { "@asyncapi/react-component": "1.0.0-next.32", "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog": "^0.7.10", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog": "^0.7.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -53,17 +53,17 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/swagger-ui-react": "^4.1.1", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 68a38461fe..882979326e 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-app-backend +## 0.3.24 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7 + +## 0.3.24-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.3.23 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.3.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + ## 0.3.22 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 9b9cfe01e7..52da00fed3 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.22", + "version": "0.3.24", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config-loader": "^0.9.3", "@backstage/config": "^0.1.13", "@backstage/types": "^0.1.1", @@ -40,15 +40,15 @@ "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", "helmet": "^4.0.0", - "knex": "^0.95.1", + "knex": "^1.0.2", "lodash": "^4.17.21", "luxon": "^2.0.2", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.15", - "@backstage/cli": "^0.13.0", + "@backstage/backend-test-utils": "^0.1.17", + "@backstage/cli": "^0.13.2", "@backstage/types": "^0.1.1", "@types/supertest": "^2.0.8", "mock-fs": "^5.1.0", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 2bbd544eea..a19153c4e9 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,166 @@ # @backstage/plugin-auth-backend +## 0.10.0 + +### Minor Changes + +- 08fcda13ef: The `callbackUrl` option of `OAuthAdapter` is now required. +- 6bc86fcf2d: The following breaking changes were made, which may imply specifically needing + to make small adjustments in your custom auth providers. + + - **BREAKING**: Moved `IdentityClient`, `BackstageSignInResult`, + `BackstageIdentityResponse`, and `BackstageUserIdentity` to + `@backstage/plugin-auth-node`. + - **BREAKING**: Removed deprecated type `BackstageIdentity`, please use + `BackstageSignInResult` from `@backstage/plugin-auth-node` instead. + + While moving over, `IdentityClient` was also changed in the following ways: + + - **BREAKING**: Made `IdentityClient.listPublicKeys` private. It was only used + in tests, and should not be part of the API surface of that class. + - **BREAKING**: Removed the static `IdentityClient.getBearerToken`. It is now + replaced by `getBearerTokenFromAuthorizationHeader` from + `@backstage/plugin-auth-node`. + - **BREAKING**: Removed the constructor. Please use the `IdentityClient.create` + static method instead. + + Since the `IdentityClient` interface is marked as experimental, this is a + breaking change without a deprecation period. + + In your auth providers, you may need to update your imports and usages as + follows (example code; yours may be slightly different): + + ````diff + -import { IdentityClient } from '@backstage/plugin-auth-backend'; + +import { + + IdentityClient, + + getBearerTokenFromAuthorizationHeader + +} from '@backstage/plugin-auth-node'; + + // ... + + - const identity = new IdentityClient({ + + const identity = IdentityClient.create({ + discovery, + issuer: await discovery.getExternalBaseUrl('auth'), + });``` + + // ... + + const token = + - IdentityClient.getBearerToken(req.headers.authorization) || + + getBearerTokenFromAuthorizationHeader(req.headers.authorization) || + req.cookies['token']; + ```` + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- 3396bc5973: Enabled refresh for the Atlassian provider. +- 08fcda13ef: Added a new `cookieConfigurer` option to `AuthProviderConfig` that makes it possible to override the default logic for configuring OAuth provider cookies. +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/backend-common@0.10.7 + - @backstage/plugin-auth-node@0.1.0 + +## 0.10.0-next.0 + +### Minor Changes + +- 08fcda13ef: The `callbackUrl` option of `OAuthAdapter` is now required. + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- 3396bc5973: Enabled refresh for the Atlassian provider. +- 08fcda13ef: Added a new `cookieConfigurer` option to `AuthProviderConfig` that makes it possible to override the default logic for configuring OAuth provider cookies. +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.9.0 + +### Minor Changes + +- cef64b1561: **BREAKING** Added `tokenManager` as a required property for the auth-backend `createRouter` function. This dependency is used to issue server tokens that are used by the `CatalogIdentityClient` when looking up users and their group membership during authentication. + + These changes are **required** to `packages/backend/src/plugins/auth.ts`: + + ```diff + export default async function createPlugin({ + logger, + database, + config, + discovery, + + tokenManager, + }: PluginEnvironment): Promise { + return await createRouter({ + logger, + config, + database, + discovery, + + tokenManager, + }); + } + ``` + + **BREAKING** The `CatalogIdentityClient` constructor now expects a `TokenManager` instead of a `TokenIssuer`. The `TokenManager` interface is used to generate a server token when [resolving a user's identity and membership through the catalog](https://backstage.io/docs/auth/identity-resolver). Using server tokens for these requests allows the auth-backend to bypass authorization checks when permissions are enabled for Backstage. This change will break apps that rely on the user tokens that were previously used by the client. Refer to the ["Backend-to-backend Authentication" tutorial](https://backstage.io/docs/tutorials/backend-to-backend-auth) for more information on server token usage. + +### Patch Changes + +- 9d75a939b6: Fixed a bug where providers that tracked the granted scopes through a cookie would not take failed authentication attempts into account. +- 28a5f9d0b1: chore(deps): bump `passport` from 0.4.1 to 0.5.2 +- 5d09bdd1de: Added custom `callbackUrl` support for multiple providers. `v0.8.0` introduced this change for `github`, and now we're adding the same capability to the following providers: `atlassian, auth0, bitbucket, gitlab, google, microsoft, oauth2, oidc, okta, onelogin`. +- 648606b3ac: Added support for storing static GitHub access tokens in cookies and using them to refresh the Backstage session. +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.9.0-next.1 + +### Patch Changes + +- 9d75a939b6: Fixed a bug where providers that tracked the granted scopes through a cookie would not take failed authentication attempts into account. +- 648606b3ac: Added support for storing static GitHub access tokens in cookies and using them to refresh the Backstage session. +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + +## 0.9.0-next.0 + +### Minor Changes + +- cef64b1561: **BREAKING** Added `tokenManager` as a required property for the auth-backend `createRouter` function. This dependency is used to issue server tokens that are used by the `CatalogIdentityClient` when looking up users and their group membership during authentication. + + These changes are **required** to `packages/backend/src/plugins/auth.ts`: + + ```diff + export default async function createPlugin({ + logger, + database, + config, + discovery, + + tokenManager, + }: PluginEnvironment): Promise { + return await createRouter({ + logger, + config, + database, + discovery, + + tokenManager, + }); + } + ``` + + **BREAKING** The `CatalogIdentityClient` constructor now expects a `TokenManager` instead of a `TokenIssuer`. The `TokenManager` interface is used to generate a server token when [resolving a user's identity and membership through the catalog](https://backstage.io/docs/auth/identity-resolver). Using server tokens for these requests allows the auth-backend to bypass authorization checks when permissions are enabled for Backstage. This change will break apps that rely on the user tokens that were previously used by the client. Refer to the ["Backend-to-backend Authentication" tutorial](https://backstage.io/docs/tutorials/backend-to-backend-auth) for more information on server token usage. + +### Patch Changes + +- 28a5f9d0b1: chore(deps): bump `passport` from 0.4.1 to 0.5.2 + ## 0.8.0 ### Minor Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 7cdc33f0e2..416da59b46 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -5,12 +5,12 @@ ```ts /// +import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { BackstageSignInResult } from '@backstage/plugin-auth-node'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { Entity } from '@backstage/catalog-model'; import express from 'express'; import { JsonValue } from '@backstage/types'; -import { JSONWebKey } from 'jose'; import { Logger as Logger_2 } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -128,30 +128,6 @@ export type AwsAlbProviderOptions = { }; }; -// @public @deprecated -export type BackstageIdentity = BackstageSignInResult; - -// @public -export interface BackstageIdentityResponse extends BackstageSignInResult { - identity: BackstageUserIdentity; -} - -// @public -export interface BackstageSignInResult { - // @deprecated - entity?: Entity; - // @deprecated - id: string; - token: string; -} - -// @public -export type BackstageUserIdentity = { - type: 'user'; - userEntityRef: string; - ownershipEntityRefs: string[]; -}; - // Warning: (ae-missing-release-tag) "BitbucketOAuthResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -214,6 +190,17 @@ export class CatalogIdentityClient { resolveCatalogMembership(query: MemberClaimQuery): Promise; } +// @public +export type CookieConfigurer = (ctx: { + providerId: string; + baseUrl: string; + callbackUrl: string; +}) => { + domain: string; + path: string; + secure: boolean; +}; + // Warning: (ae-missing-release-tag) "createAtlassianProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -412,20 +399,6 @@ export type GoogleProviderOptions = { }; }; -// Warning: (ae-missing-release-tag) "IdentityClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export class IdentityClient { - constructor(options: { discovery: PluginEndpointDiscovery; issuer: string }); - authenticate(token: string | undefined): Promise; - static getBearerToken( - authorizationHeader: string | undefined, - ): string | undefined; - listPublicKeys(): Promise<{ - keys: JSONWebKey[]; - }>; -} - // Warning: (ae-missing-release-tag) "microsoftEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -592,6 +565,7 @@ export type OAuthState = { nonce: string; env: string; origin?: string; + scope?: string; }; // @public @@ -734,6 +708,6 @@ export type WebMessageResponse = // // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts // src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts -// src/providers/github/provider.d.ts:81:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:98:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts +// src/providers/github/provider.d.ts:97:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:118:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/auth-backend/migrations/20210326100300_timestamptz.js b/plugins/auth-backend/migrations/20210326100300_timestamptz.js index e9bdddde8f..144f450380 100644 --- a/plugins/auth-backend/migrations/20210326100300_timestamptz.js +++ b/plugins/auth-backend/migrations/20210326100300_timestamptz.js @@ -28,7 +28,7 @@ exports.up = async function up(knex) { .notNullable() .defaultTo(knex.fn.now()) .comment('The creation time of the key') - .alter(); + .alter({ alterType: true }); }); } }; @@ -45,7 +45,7 @@ exports.down = async function down(knex) { .notNullable() .defaultTo(knex.fn.now()) .comment('The creation time of the key') - .alter(); + .alter({ alterType: true }); }); } }; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 5b4e4b22ba..5d40523895 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.8.0", + "version": "0.10.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/catalog-client": "^0.5.5", + "@backstage/plugin-auth-node": "^0.1.0", + "@backstage/backend-common": "^0.10.7", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", @@ -50,12 +51,12 @@ "helmet": "^4.0.0", "jose": "^1.27.1", "jwt-decode": "^3.1.0", - "knex": "^0.95.1", + "knex": "^1.0.2", "lodash": "^4.17.21", "luxon": "^2.0.2", "minimatch": "^3.0.3", "morgan": "^1.10.0", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "node-cache": "^5.1.2", "openid-client": "^4.2.1", "passport": "^0.5.2", @@ -64,7 +65,7 @@ "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", "passport-microsoft": "^0.1.0", - "passport-oauth2": "^1.5.0", + "passport-oauth2": "^1.6.1", "passport-okta-oauth": "^0.0.1", "passport-onelogin-oauth": "^0.0.1", "passport-saml": "^3.1.2", @@ -73,8 +74,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/test-utils": "^0.2.4", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/auth-backend/src/identity/index.ts b/plugins/auth-backend/src/identity/index.ts index 73858d7e07..cb2f369668 100644 --- a/plugins/auth-backend/src/identity/index.ts +++ b/plugins/auth-backend/src/identity/index.ts @@ -15,7 +15,6 @@ */ export { createOidcRouter } from './router'; -export { IdentityClient } from './IdentityClient'; export { TokenFactory } from './TokenFactory'; export { DatabaseKeyStore } from './DatabaseKeyStore'; export { MemoryKeyStore } from './MemoryKeyStore'; diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts index 66c1b42dd9..d0cade087e 100644 --- a/plugins/auth-backend/src/index.ts +++ b/plugins/auth-backend/src/index.ts @@ -21,7 +21,6 @@ */ export * from './service/router'; -export { IdentityClient } from './identity'; export type { TokenIssuer } from './identity'; export * from './providers'; diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index a85dd49fba..c1130b4270 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -17,7 +17,7 @@ import express from 'express'; import { THOUSAND_DAYS_MS, TEN_MINUTES_MS, OAuthAdapter } from './OAuthAdapter'; import { encodeState } from './helpers'; -import { OAuthHandlers, OAuthResponse } from './types'; +import { OAuthHandlers, OAuthResponse, OAuthState } from './types'; const mockResponseData = { providerInfo: { @@ -69,13 +69,14 @@ describe('OAuthAdapter', () => { secure: false, disableRefresh: true, appOrigin: 'http://localhost:3000', - cookieDomain: 'localhost', + cookieDomain: 'example.com', cookiePath: '/auth/test-provider', tokenIssuer: { issueToken: async () => 'my-id-token', listPublicKeys: async () => ({ keys: [] }), }, isOriginAllowed: () => false, + callbackUrl: 'http://example.com:7007/auth/test-provider/frame/handler', }; it('sets the correct headers in start', async () => { @@ -148,6 +149,102 @@ describe('OAuthAdapter', () => { ); }); + it('persists scope through cookie if enabled', async () => { + const handlers = { + start: jest.fn(async (_req: { state: OAuthState }) => ({ + url: '/url', + status: 301, + })), + handler: jest.fn(async () => ({ response: mockResponseData })), + refresh: jest.fn(async () => ({ response: mockResponseData })), + }; + const oauthProvider = new OAuthAdapter(handlers, { + ...oAuthProviderOptions, + disableRefresh: false, + persistScopes: true, + }); + + // First we test the /start request, making sure state is set + const mockStartReq = { + query: { + scope: 'user', + env: 'development', + }, + } as unknown as express.Request; + const mockStartRes = { + cookie: jest.fn().mockReturnThis(), + end: jest.fn().mockReturnThis(), + setHeader: jest.fn().mockReturnThis(), + statusCode: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + await oauthProvider.start(mockStartReq, mockStartRes); + + expect(handlers.start).toHaveBeenCalledTimes(1); + expect(handlers.start).toHaveBeenCalledWith({ + query: { + scope: 'user', + env: 'development', + }, + scope: 'user', + state: { + nonce: expect.any(String), + env: 'development', + origin: undefined, + scope: 'user', + }, + }); + + // Then test the /handler, making sure the granted scope cookie is set + const providedState = handlers.start.mock.calls[0][0].state; + const mockHandleReq = { + cookies: { + 'test-provider-nonce': providedState.nonce, + }, + query: { + state: encodeState(providedState), + }, + } as unknown as express.Request; + const mockHandleRes = { + cookie: jest.fn().mockReturnThis(), + setHeader: jest.fn().mockReturnThis(), + end: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + await oauthProvider.frameHandler(mockHandleReq, mockHandleRes); + expect(mockHandleRes.cookie).toHaveBeenCalledTimes(1); + expect(mockHandleRes.cookie).toHaveBeenCalledWith( + 'test-provider-granted-scope', + 'user', + expect.objectContaining({ + path: '/auth/test-provider', + maxAge: THOUSAND_DAYS_MS, + }), + ); + + // Them make sure scopes are forwarded correctly during refresh + const mockRefreshReq = { + query: { scope: 'ignore-me' }, + cookies: { + 'test-provider-granted-scope': 'user', + 'test-provider-refresh-token': 'refresh-token', + }, + header: jest.fn().mockReturnValue('XMLHttpRequest'), + } as unknown as express.Request; + const mockRefreshRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + } as unknown as express.Response; + await oauthProvider.refresh(mockRefreshReq, mockRefreshRes); + expect(handlers.refresh).toHaveBeenCalledTimes(1); + expect(handlers.refresh).toHaveBeenCalledWith( + expect.objectContaining({ + scope: 'user', + refreshToken: 'refresh-token', + }), + ); + }); + it('does not set the refresh cookie if refresh is disabled', async () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, @@ -348,47 +445,6 @@ describe('OAuthAdapter', () => { }); }); - it('sets the correct cookie configuration using the base url', async () => { - const config = { - baseUrl: 'http://domain.org/auth', - appUrl: 'http://domain.org', - isOriginAllowed: () => false, - }; - - const oauthProvider = OAuthAdapter.fromConfig( - config, - providerInstance, - oAuthProviderOptions, - ); - - const mockRequest = { - query: { - scope: 'user', - env: 'development', - }, - } as unknown as express.Request; - - const mockResponse = { - cookie: jest.fn().mockReturnThis(), - end: jest.fn().mockReturnThis(), - setHeader: jest.fn().mockReturnThis(), - statusCode: jest.fn().mockReturnThis(), - } as unknown as express.Response; - - await oauthProvider.start(mockRequest, mockResponse); - - expect(mockResponse.cookie).toBeCalledTimes(1); - expect(mockResponse.cookie).toBeCalledWith( - `${oAuthProviderOptions.providerId}-nonce`, - expect.any(String), - expect.objectContaining({ - domain: 'domain.org', - path: '/auth/test-provider/handler', - secure: false, - }), - ); - }); - it('sets the correct cookie configuration using a callbackUrl', async () => { const config = { baseUrl: 'http://domain.org/auth', diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 564d5c20de..75a3605483 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import express from 'express'; +import express, { CookieOptions } from 'express'; import crypto from 'crypto'; import { URL } from 'url'; import { @@ -23,10 +23,12 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { - AuthProviderRouteHandlers, - AuthProviderConfig, BackstageIdentityResponse, BackstageSignInResult, +} from '@backstage/plugin-auth-node'; +import { + AuthProviderRouteHandlers, + AuthProviderConfig, } from '../../providers/types'; import { AuthenticationError, @@ -35,7 +37,7 @@ import { NotAllowedError, } from '@backstage/errors'; import { TokenIssuer } from '../../identity/types'; -import { getCookieConfig, readState, verifyNonce } from './helpers'; +import { defaultCookieConfigurer, readState, verifyNonce } from './helpers'; import { postMessageResponse, ensuresXRequestedWith } from '../flow'; import { OAuthHandlers, @@ -58,7 +60,7 @@ export type Options = { appOrigin: string; tokenIssuer: TokenIssuer; isOriginAllowed: (origin: string) => boolean; - callbackUrl?: string; + callbackUrl: string; }; export class OAuthAdapter implements AuthProviderRouteHandlers { static fromConfig( @@ -74,26 +76,38 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { >, ): OAuthAdapter { const { origin: appOrigin } = new URL(config.appUrl); - const authUrl = new URL(options.callbackUrl ?? config.baseUrl); - const { cookieDomain, cookiePath, secure } = getCookieConfig( - authUrl, - options.providerId, - ); + + const cookieConfigurer = config.cookieConfigurer ?? defaultCookieConfigurer; + const cookieConfig = cookieConfigurer({ + providerId: options.providerId, + baseUrl: config.baseUrl, + callbackUrl: options.callbackUrl, + }); return new OAuthAdapter(handlers, { ...options, appOrigin, - cookieDomain, - cookiePath, - secure, + cookieDomain: cookieConfig.domain, + cookiePath: cookieConfig.path, + secure: cookieConfig.secure, isOriginAllowed: config.isOriginAllowed, }); } + private readonly baseCookieOptions: CookieOptions; + constructor( private readonly handlers: OAuthHandlers, private readonly options: Options, - ) {} + ) { + this.baseCookieOptions = { + httpOnly: true, + sameSite: 'lax', + secure: this.options.secure, + path: this.options.cookiePath, + domain: this.options.cookieDomain, + }; + } async start(req: express.Request, res: express.Response): Promise { // retrieve scopes from request @@ -105,15 +119,17 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { throw new InputError('No env provided in request query parameters'); } - if (this.options.persistScopes) { - this.setScopesCookie(res, scope); - } - const nonce = crypto.randomBytes(16).toString('base64'); // set a nonce cookie before redirecting to oauth provider this.setNonceCookie(res, nonce); - const state = { nonce, env, origin }; + const state: OAuthState = { nonce, env, origin }; + + // If scopes are persisted then we pass them through the state so that we + // can set the cookie on successful auth + if (this.options.persistScopes) { + state.scope = scope; + } const forwardReq = Object.assign(req, { scope, state }); const { url, status } = await this.handlers.start( @@ -151,12 +167,11 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { const { response, refreshToken } = await this.handlers.handler(req); - if (this.options.persistScopes) { - const grantedScopes = this.getScopesFromCookie( - req, - this.options.providerId, - ); - response.providerInfo.scope = grantedScopes; + // Store the scope that we have been granted for this session. This is useful if + // the provider does not return granted scopes on refresh or if they are normalized. + if (this.options.persistScopes && state.scope) { + this.setGrantedScopeCookie(res, state.scope); + response.providerInfo.scope = state.scope; } if (refreshToken && !this.options.disableRefresh) { @@ -214,8 +229,10 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { throw new InputError('Missing session cookie'); } - const scope = req.query.scope?.toString() ?? ''; - + let scope = req.query.scope?.toString() ?? ''; + if (this.options.persistScopes) { + scope = this.getGrantedScopeFromCookie(req); + } const forwardReq = Object.assign(req, { scope, refreshToken }); // get new access_token @@ -267,27 +284,20 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { private setNonceCookie = (res: express.Response, nonce: string) => { res.cookie(`${this.options.providerId}-nonce`, nonce, { maxAge: TEN_MINUTES_MS, - secure: this.options.secure, - sameSite: 'lax', - domain: this.options.cookieDomain, + ...this.baseCookieOptions, path: `${this.options.cookiePath}/handler`, - httpOnly: true, }); }; - private setScopesCookie = (res: express.Response, scope: string) => { - res.cookie(`${this.options.providerId}-scope`, scope, { - maxAge: TEN_MINUTES_MS, - secure: this.options.secure, - sameSite: 'lax', - domain: this.options.cookieDomain, - path: `${this.options.cookiePath}/handler`, - httpOnly: true, + private setGrantedScopeCookie = (res: express.Response, scope: string) => { + res.cookie(`${this.options.providerId}-granted-scope`, scope, { + maxAge: THOUSAND_DAYS_MS, + ...this.baseCookieOptions, }); }; - private getScopesFromCookie = (req: express.Request, providerId: string) => { - return req.cookies[`${providerId}-scope`]; + private getGrantedScopeFromCookie = (req: express.Request) => { + return req.cookies[`${this.options.providerId}-granted-scope`]; }; private setRefreshTokenCookie = ( @@ -296,22 +306,14 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { ) => { res.cookie(`${this.options.providerId}-refresh-token`, refreshToken, { maxAge: THOUSAND_DAYS_MS, - secure: this.options.secure, - sameSite: 'lax', - domain: this.options.cookieDomain, - path: this.options.cookiePath, - httpOnly: true, + ...this.baseCookieOptions, }); }; private removeRefreshTokenCookie = (res: express.Response) => { res.cookie(`${this.options.providerId}-refresh-token`, '', { maxAge: 0, - secure: this.options.secure, - sameSite: 'lax', - domain: this.options.cookieDomain, - path: this.options.cookiePath, - httpOnly: true, + ...this.baseCookieOptions, }); }; } diff --git a/plugins/auth-backend/src/lib/oauth/helpers.test.ts b/plugins/auth-backend/src/lib/oauth/helpers.test.ts index a98b684141..2f79cdef98 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.test.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.test.ts @@ -19,7 +19,7 @@ import { verifyNonce, encodeState, readState, - getCookieConfig, + defaultCookieConfigurer, } from './helpers'; describe('OAuthProvider Utils', () => { @@ -110,30 +110,43 @@ describe('OAuthProvider Utils', () => { }); }); - describe('getCookieConfig', () => { + describe('defaultCookieConfigurer', () => { it('should set the correct domain and path for a base url', () => { - const mockAuthUrl = new URL('http://domain.org/auth'); - expect(getCookieConfig(mockAuthUrl, 'test-provider')).toMatchObject({ - cookieDomain: 'domain.org', - cookiePath: '/auth/test-provider', + expect( + defaultCookieConfigurer({ + baseUrl: '', + providerId: 'test-provider', + callbackUrl: 'http://domain.org/auth', + }), + ).toMatchObject({ + domain: 'domain.org', + path: '/auth/test-provider', secure: false, }); }); it('should set the correct domain and path for a url containing a frame handler', () => { - const mockAuthUrl = new URL( - 'http://domain.org/auth/test-provider/handler/frame', - ); - expect(getCookieConfig(mockAuthUrl, 'test-provider')).toMatchObject({ - cookieDomain: 'domain.org', - cookiePath: '/auth/test-provider', + expect( + defaultCookieConfigurer({ + baseUrl: '', + providerId: 'test-provider', + callbackUrl: 'http://domain.org/auth/test-provider/handler/frame', + }), + ).toMatchObject({ + domain: 'domain.org', + path: '/auth/test-provider', secure: false, }); }); it('should set the secure flag if url is using https', () => { - const mockAuthUrl = new URL('https://domain.org/auth'); - expect(getCookieConfig(mockAuthUrl, 'test-provider')).toMatchObject({ + expect( + defaultCookieConfigurer({ + baseUrl: '', + providerId: 'test-provider', + callbackUrl: 'https://domain.org/auth', + }), + ).toMatchObject({ secure: true, }); }); diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index 878083e679..eec25696a3 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -17,6 +17,7 @@ import express from 'express'; import { OAuthState } from './types'; import pickBy from 'lodash/pickBy'; +import { CookieConfigurer } from '../../providers/types'; export const readState = (stateString: string): OAuthState => { const state = Object.fromEntries( @@ -58,20 +59,19 @@ export const verifyNonce = (req: express.Request, providerId: string) => { } }; -export const getCookieConfig = (authUrl: URL, providerId: string) => { - const { hostname: cookieDomain, pathname, protocol } = authUrl; +export const defaultCookieConfigurer: CookieConfigurer = ({ + callbackUrl, + providerId, +}) => { + const { hostname: domain, pathname, protocol } = new URL(callbackUrl); const secure = protocol === 'https:'; // If the provider supports callbackUrls, the pathname will // contain the complete path to the frame handler so we need // to slice off the trailing part of the path. - const cookiePath = pathname.endsWith(`${providerId}/handler/frame`) + const path = pathname.endsWith(`${providerId}/handler/frame`) ? pathname.slice(0, -'/handler/frame'.length) : `${pathname}/${providerId}`; - return { - cookieDomain, - cookiePath, - secure, - }; + return { domain, path, secure }; }; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 9ddef007a7..6e5fe19859 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -16,11 +16,8 @@ import express from 'express'; import { Profile as PassportProfile } from 'passport'; -import { - RedirectInfo, - BackstageSignInResult, - ProfileInfo, -} from '../../providers/types'; +import { BackstageSignInResult } from '@backstage/plugin-auth-node'; +import { RedirectInfo, ProfileInfo } from '../../providers/types'; /** * Common options for passport.js-based OAuth providers @@ -87,6 +84,7 @@ export type OAuthState = { nonce: string; env: string; origin?: string; + scope?: string; }; export type OAuthStartRequest = express.Request<{}> & { diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts index 2696fd233f..26eea420fc 100644 --- a/plugins/auth-backend/src/providers/atlassian/provider.ts +++ b/plugins/auth-backend/src/providers/atlassian/provider.ts @@ -205,7 +205,10 @@ export const createAtlassianProvider = ( const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const scopes = envConfig.getString('scopes'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, @@ -228,9 +231,9 @@ export const createAtlassianProvider = ( }); return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, providerId, tokenIssuer, + callbackUrl, }); }); }; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 15578117ea..fbd57c0b76 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -228,7 +228,10 @@ export const createAuth0Provider = ( const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const domain = envConfig.getString('domain'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, @@ -259,6 +262,7 @@ export const createAuth0Provider = ( disableRefresh: true, providerId, tokenIssuer, + callbackUrl, }); }); }; diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts index 4d5bdcf6f6..1cc0e60bd8 100644 --- a/plugins/auth-backend/src/providers/bitbucket/provider.ts +++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts @@ -280,7 +280,10 @@ export const createBitbucketProvider = ( OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, @@ -309,6 +312,7 @@ export const createBitbucketProvider = ( disableRefresh: false, providerId, tokenIssuer, + callbackUrl, }); }); }; diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index 15eeccc07c..206df52083 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -98,6 +98,7 @@ describe('GithubAuthProvider', () => { providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', scope: 'read:scope', + expiresInSeconds: 3600, }, profile: { email: 'jimmymarkum@gmail.com', @@ -143,6 +144,7 @@ describe('GithubAuthProvider', () => { providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', scope: 'read:scope', + expiresInSeconds: 3600, }, profile: { displayName: 'Jimmy Markum', @@ -186,6 +188,7 @@ describe('GithubAuthProvider', () => { providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', scope: 'read:scope', + expiresInSeconds: 3600, }, profile: { displayName: 'jimmymarkum', @@ -230,6 +233,7 @@ describe('GithubAuthProvider', () => { accessToken: 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', scope: 'read:user', + expiresInSeconds: 3600, }, profile: { displayName: 'Dave Boyle', @@ -316,7 +320,7 @@ describe('GithubAuthProvider', () => { ], }); - const result = await provider.refresh({} as any); + const result = await provider.refresh({ scope: 'actual-scope' } as any); expect(result).toEqual({ response: { @@ -332,11 +336,65 @@ describe('GithubAuthProvider', () => { providerInfo: { accessToken: 'a.b.c', expiresInSeconds: 123, - scope: 'read_user', + scope: 'actual-scope', }, }, refreshToken: 'dont-forget-to-send-refresh', }); + + mockRefreshToken.mockRestore(); + mockUserProfile.mockRestore(); + }); + + it('should use access token as refresh token', async () => { + const mockUserProfile = jest.spyOn( + helpers, + 'executeFetchUserProfileStrategy', + ) as unknown as jest.MockedFunction<() => Promise>; + + mockUserProfile.mockResolvedValueOnce({ + id: 'mockid', + username: 'mockuser', + provider: 'github', + displayName: 'Mocked User', + emails: [ + { + value: 'mockuser@gmail.com', + }, + ], + }); + + const result = await provider.refresh({ + refreshToken: 'access-token.le-token', + scope: 'the-scope', + } as any); + + expect(mockUserProfile).toHaveBeenCalledTimes(1); + expect(mockUserProfile).toHaveBeenCalledWith( + expect.anything(), + 'le-token', + ); + expect(result).toEqual({ + response: { + backstageIdentity: { + id: 'mockuser', + token: 'token-for-user:default/mockuser', + }, + profile: { + displayName: 'Mocked User', + email: 'mockuser@gmail.com', + picture: undefined, + }, + providerInfo: { + accessToken: 'le-token', + expiresInSeconds: 3600, + scope: 'the-scope', + }, + }, + refreshToken: 'access-token.le-token', + }); + + mockUserProfile.mockRestore(); }); }); }); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 9dc70cf60e..ec7f8cafa6 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -41,11 +41,15 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, - OAuthResponse, } from '../../lib/oauth'; import { CatalogIdentityClient } from '../../lib/catalog'; import { TokenIssuer } from '../../identity'; +const ACCESS_TOKEN_PREFIX = 'access-token.'; + +// TODO(Rugvip): Auth providers need a way to access this in a less hardcoded way +const BACKSTAGE_SESSION_EXPIRATION = 3600; + type PrivateInfo = { refreshToken?: string; }; @@ -123,31 +127,69 @@ export class GithubAuthProvider implements OAuthHandlers { PrivateInfo >(req, this._strategy); + let refreshToken = privateInfo.refreshToken; + + // If we do not have a real refresh token and we have a non-expiring + // access token, then we use that as our refresh token. + if (!refreshToken && !result.params.expires_in) { + refreshToken = ACCESS_TOKEN_PREFIX + result.accessToken; + } + return { response: await this.handleResult(result), - refreshToken: privateInfo.refreshToken, + refreshToken, }; } async refresh(req: OAuthRefreshRequest) { - const { accessToken, refreshToken, params } = - await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - req.scope, - ); - const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, - accessToken, - ); + // We've enable persisting scope in the OAuth provider, so scope here will + // be whatever was stored in the cookie + const { scope, refreshToken } = req; + // This is the OAuth App flow. A non-expiring access token is stored in the + // refresh token cookie. We use that token to fetch the user profile and + // refresh the Backstage session when needed. + if (refreshToken?.startsWith(ACCESS_TOKEN_PREFIX)) { + const accessToken = refreshToken.slice(ACCESS_TOKEN_PREFIX.length); + + const fullProfile = await executeFetchUserProfileStrategy( + this._strategy, + accessToken, + ).catch(error => { + if (error.oauthError?.statusCode === 401) { + throw new Error('Invalid access token'); + } + throw error; + }); + + return { + response: await this.handleResult({ + fullProfile, + params: { scope }, + accessToken, + }), + refreshToken, + }; + } + + // This is the App flow, which is close to a standard OAuth refresh flow. It has a + // pretty long session expiration, and it also ignores the requested scope, instead + // just allowing access to whatever is configured as part of the app installation. + const result = await executeRefreshTokenStrategy( + this._strategy, + refreshToken, + scope, + ); return { response: await this.handleResult({ - fullProfile, - params, - accessToken, + fullProfile: await executeFetchUserProfileStrategy( + this._strategy, + result.accessToken, + ), + params: { ...result.params, scope }, + accessToken: result.accessToken, }), - refreshToken, + refreshToken: result.refreshToken, }; } @@ -160,27 +202,41 @@ export class GithubAuthProvider implements OAuthHandlers { const { profile } = await this.authHandler(result, context); const expiresInStr = result.params.expires_in; - const response: OAuthResponse = { - providerInfo: { - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: - expiresInStr === undefined ? undefined : Number(expiresInStr), - }, - profile, - }; + let expiresInSeconds = + expiresInStr === undefined ? undefined : Number(expiresInStr); + + let backstageIdentity = undefined; if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( + backstageIdentity = await this.signInResolver( { result, profile, }, context, ); + + // GitHub sessions last longer than Backstage sessions, so if we're using + // GitHub for sign-in, then we need to expire the sessions earlier + if (expiresInSeconds) { + expiresInSeconds = Math.min( + expiresInSeconds, + BACKSTAGE_SESSION_EXPIRATION, + ); + } else { + expiresInSeconds = BACKSTAGE_SESSION_EXPIRATION; + } } - return response; + return { + backstageIdentity, + providerInfo: { + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds, + }, + profile, + }; } } diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index da816f2fd2..6689aba884 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -236,7 +236,10 @@ export const createGitlabProvider = ( const clientSecret = envConfig.getString('clientSecret'); const audience = envConfig.getOptionalString('audience'); const baseUrl = audience || 'https://gitlab.com'; - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, @@ -272,6 +275,7 @@ export const createGitlabProvider = ( disableRefresh: false, providerId, tokenIssuer, + callbackUrl, }); }); }; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 13c5093aa5..fbaf8c7462 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -266,7 +266,10 @@ export const createGoogleProvider = ( OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, @@ -304,6 +307,7 @@ export const createGoogleProvider = ( disableRefresh: false, providerId, tokenIssuer, + callbackUrl, }); }); }; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 7779b67e04..78814ae7e6 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -43,17 +43,11 @@ export type { AuthHandlerResult, SignInResolver, SignInInfo, + CookieConfigurer, } from './types'; // These types are needed for a postMessage from the login pop-up // to the frontend -export type { - AuthResponse, - BackstageIdentity, - BackstageUserIdentity, - BackstageIdentityResponse, - BackstageSignInResult, - ProfileInfo, -} from './types'; +export type { AuthResponse, ProfileInfo } from './types'; export { prepareBackstageIdentityResponse } from './prepareBackstageIdentityResponse'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 9249f643dc..25c940547b 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -276,7 +276,10 @@ export const createMicrosoftProvider = ( const clientSecret = envConfig.getString('clientSecret'); const tenantId = envConfig.getString('tenantId'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`; const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; @@ -318,6 +321,7 @@ export const createMicrosoftProvider = ( disableRefresh: false, providerId, tokenIssuer, + callbackUrl, }); }); }; diff --git a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts index 24bb4d0362..8c4dcc3249 100644 --- a/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2-proxy/provider.ts @@ -17,6 +17,7 @@ import express from 'express'; import { Logger } from 'winston'; import { AuthenticationError } from '@backstage/errors'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { AuthHandler, SignInResolver, @@ -26,7 +27,6 @@ import { } from '../types'; import { CatalogIdentityClient } from '../../lib/catalog'; import { JWT } from 'jose'; -import { IdentityClient } from '../../identity'; import { TokenIssuer } from '../../identity/types'; import { prepareBackstageIdentityResponse } from '../prepareBackstageIdentityResponse'; @@ -156,7 +156,7 @@ export class Oauth2ProxyAuthProvider private getResult(req: express.Request): OAuth2ProxyResult { const authHeader = req.header(OAUTH2_PROXY_JWT_HEADER); - const jwt = IdentityClient.getBearerToken(authHeader); + const jwt = getBearerTokenFromAuthorizationHeader(authHeader); if (!jwt) { throw new AuthenticationError( diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 8e69a3ee1d..0906005791 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -240,7 +240,10 @@ export const createOAuth2Provider = ( OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; const authorizationUrl = envConfig.getString('authorizationUrl'); const tokenUrl = envConfig.getString('tokenUrl'); const scope = envConfig.getOptionalString('scope'); @@ -288,6 +291,7 @@ export const createOAuth2Provider = ( disableRefresh, providerId, tokenIssuer, + callbackUrl, }); }); }; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index fa46294784..9ccdf25f74 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -234,7 +234,7 @@ export const oAuth2DefaultSignInResolver: SignInResolver< * can be passed while creating a OIDC provider. * * authHandler : called after sign in was successful, a new object must be returned which includes a profile - * signInResolver: called after sign in was successful, expects to return a new {@link BackstageSignInResult} + * signInResolver: called after sign in was successful, expects to return a new {@link @backstage/plugin-auth-node#BackstageSignInResult} * * Both options are optional. There is fallback for authHandler where the default handler expect an e-mail explicitly * otherwise it throws an error @@ -264,7 +264,10 @@ export const createOidcProvider = ( OAuthEnvironmentHandler.mapConfig(config, envConfig => { const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; const metadataUrl = envConfig.getString('metadataUrl'); const tokenSignedResponseAlg = envConfig.getOptionalString( 'tokenSignedResponseAlg', @@ -313,6 +316,7 @@ export const createOidcProvider = ( disableRefresh: false, providerId, tokenIssuer, + callbackUrl, }); }); }; diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index def0da694e..0ba7d241d3 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -276,7 +276,10 @@ export const createOktaProvider = ( const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const audience = envConfig.getString('audience'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; // This is a safe assumption as `passport-okta-oauth` uses the audience // as the base for building the authorization, token, and user info URLs. @@ -322,6 +325,7 @@ export const createOktaProvider = ( disableRefresh: false, providerId, tokenIssuer, + callbackUrl, }); }); }; diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index a5ab4f658b..e14e8548a0 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -227,7 +227,10 @@ export const createOneLoginProvider = ( const clientId = envConfig.getString('clientId'); const clientSecret = envConfig.getString('clientSecret'); const issuer = envConfig.getString('issuer'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; const catalogIdentityClient = new CatalogIdentityClient({ catalogApi, @@ -258,6 +261,7 @@ export const createOneLoginProvider = ( disableRefresh: false, providerId, tokenIssuer, + callbackUrl, }); }); }; diff --git a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts index bb99cb3487..f3b234c08e 100644 --- a/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts +++ b/plugins/auth-backend/src/providers/prepareBackstageIdentityResponse.ts @@ -19,7 +19,10 @@ import { parseEntityRef, stringifyEntityRef, } from '@backstage/catalog-model'; -import { BackstageIdentityResponse, BackstageSignInResult } from './types'; +import { + BackstageIdentityResponse, + BackstageSignInResult, +} from '@backstage/plugin-auth-node'; function parseJwtPayload(token: string) { const [_header, payload, _signature] = token.split('.'); @@ -28,7 +31,7 @@ function parseJwtPayload(token: string) { /** * Parses a Backstage-issued token and decorates the - * {@link BackstageIdentityResponse} with identity information sourced from the + * {@link @backstage/plugin-auth-node#BackstageIdentityResponse} with identity information sourced from the * token. * * @public diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index b02e8d74b8..5bd52f0c94 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -19,8 +19,11 @@ import { TokenManager, } from '@backstage/backend-common'; import { CatalogApi } from '@backstage/catalog-client'; -import { Entity } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +import { + BackstageIdentityResponse, + BackstageSignInResult, +} from '@backstage/plugin-auth-node'; import express from 'express'; import { Logger } from 'winston'; import { TokenIssuer } from '../identity/types'; @@ -38,6 +41,19 @@ export type AuthResolverContext = { logger: Logger; }; +/** + * The callback used to resolve the cookie configuration for auth providers that use cookies. + * @public + */ +export type CookieConfigurer = (ctx: { + /** ID of the auth provider that this configuration applies to */ + providerId: string; + /** The externally reachable base URL of the auth-backend plugin */ + baseUrl: string; + /** The configured callback URL of the auth provider */ + callbackUrl: string; +}) => { domain: string; path: string; secure: boolean }; + export type AuthProviderConfig = { /** * The protocol://domain[:port] where the app is hosted. This is used to construct the @@ -54,6 +70,11 @@ export type AuthProviderConfig = { * A function that is called to check whether an origin is allowed to receive the authentication result. */ isOriginAllowed: (origin: string) => boolean; + + /** + * The function used to resolve cookie configuration based on the auth provider options. + */ + cookieConfigurer?: CookieConfigurer; }; export type RedirectInfo = { @@ -143,85 +164,6 @@ export type AuthResponse = { backstageIdentity?: BackstageIdentityResponse; }; -/** - * User identity information within Backstage. - * - * @public - */ -export type BackstageUserIdentity = { - /** - * The type of identity that this structure represents. In the frontend app - * this will currently always be 'user'. - */ - type: 'user'; - - /** - * The entityRef of the user in the catalog. - * For example User:default/sandra - */ - userEntityRef: string; - - /** - * The user and group entities that the user claims ownership through - */ - ownershipEntityRefs: string[]; -}; - -/** - * A representation of a successful Backstage sign-in. - * - * Compared to the {@link BackstageIdentityResponse} this type omits - * the decoded identity information embedded in the token. - * - * @public - */ -export interface BackstageSignInResult { - /** - * An opaque ID that uniquely identifies the user within Backstage. - * - * This is typically the same as the user entity `metadata.name`. - * - * @deprecated Use the `identity` field instead - */ - id: string; - - /** - * The entity that the user is represented by within Backstage. - * - * This entity may or may not exist within the Catalog, and it can be used - * to read and store additional metadata about the user. - * - * @deprecated Use the `identity` field instead. - */ - entity?: Entity; - - /** - * The token used to authenticate the user within Backstage. - */ - token: string; -} - -/** - * The old exported symbol for {@link BackstageSignInResult}. - * - * @public - * @deprecated Use the {@link BackstageSignInResult} instead. - */ -export type BackstageIdentity = BackstageSignInResult; - -/** - * Response object containing the {@link BackstageUserIdentity} and the token - * from the authentication provider. - * - * @public - */ -export interface BackstageIdentityResponse extends BackstageSignInResult { - /** - * A plaintext description of the identity that is encapsulated within the token. - */ - identity: BackstageUserIdentity; -} - /** * Used to display login information to user, i.e. sidebar popup. * @@ -268,7 +210,7 @@ export type SignInInfo = { /** * Describes the function which handles the result of a successful - * authentication. Must return a valid {@link BackstageSignInResult}. + * authentication. Must return a valid {@link @backstage/plugin-auth-node#BackstageSignInResult}. * * @public */ diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index bdb68929b1..ec1a52ffc9 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -111,7 +111,11 @@ export async function createRouter( try { const provider = providerFactory({ providerId, - globalConfig: { baseUrl: authUrl, appUrl, isOriginAllowed }, + globalConfig: { + baseUrl: authUrl, + appUrl, + isOriginAllowed, + }, config: providersConfig.getConfig(providerId), logger, tokenManager, diff --git a/plugins/auth-node/.eslintrc.js b/plugins/auth-node/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/auth-node/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md new file mode 100644 index 0000000000..a5103ea034 --- /dev/null +++ b/plugins/auth-node/CHANGELOG.md @@ -0,0 +1,13 @@ +# @backstage/plugin-auth-node + +## 0.1.0 + +### Minor Changes + +- 9058bb1b5e: Added this package, to hold shared types and functionality that other backend + packages need to import. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 diff --git a/plugins/auth-node/README.md b/plugins/auth-node/README.md new file mode 100644 index 0000000000..3558e031b2 --- /dev/null +++ b/plugins/auth-node/README.md @@ -0,0 +1,3 @@ +# Auth Node + +Common functionality and types for the Backstage `auth` plugin. diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md new file mode 100644 index 0000000000..7840fc7d74 --- /dev/null +++ b/plugins/auth-node/api-report.md @@ -0,0 +1,43 @@ +## API Report File for "@backstage/plugin-auth-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Entity } from '@backstage/catalog-model'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// @public +export interface BackstageIdentityResponse extends BackstageSignInResult { + identity: BackstageUserIdentity; +} + +// @public +export interface BackstageSignInResult { + // @deprecated + entity?: Entity; + // @deprecated + id: string; + token: string; +} + +// @public +export type BackstageUserIdentity = { + type: 'user'; + userEntityRef: string; + ownershipEntityRefs: string[]; +}; + +// @public +export function getBearerTokenFromAuthorizationHeader( + authorizationHeader: unknown, +): string | undefined; + +// @public +export class IdentityClient { + authenticate(token: string | undefined): Promise; + static create(options: { + discovery: PluginEndpointDiscovery; + issuer: string; + }): IdentityClient; +} +``` diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json new file mode 100644 index 0000000000..4e059c5e19 --- /dev/null +++ b/plugins/auth-node/package.json @@ -0,0 +1,38 @@ +{ + "name": "@backstage/plugin-auth-node", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.10.7", + "@backstage/catalog-model": "^0.9.10", + "@backstage/config": "^0.1.13", + "@backstage/errors": "^0.2.0", + "jose": "^1.27.1", + "node-fetch": "^2.6.7", + "winston": "^3.2.1" + }, + "devDependencies": { + "@backstage/cli": "^0.13.2", + "msw": "^0.35.0", + "uuid": "^8.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/auth-backend/src/identity/IdentityClient.test.ts b/plugins/auth-node/src/IdentityClient.test.ts similarity index 76% rename from plugins/auth-backend/src/identity/IdentityClient.test.ts rename to plugins/auth-node/src/IdentityClient.test.ts index 9f5e1ce489..72ef7f2a57 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.test.ts +++ b/plugins/auth-node/src/IdentityClient.test.ts @@ -14,19 +14,61 @@ * limitations under the License. */ -import { JWT, JSONWebKey } from 'jose'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { JSONWebKey, JWK, JWS, JWT } from 'jose'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { - getVoidLogger, - PluginEndpointDiscovery, -} from '@backstage/backend-common'; +import { v4 as uuid } from 'uuid'; import { IdentityClient } from './IdentityClient'; -import { MemoryKeyStore } from './MemoryKeyStore'; -import { TokenFactory } from './TokenFactory'; -import { KeyStore } from './types'; -const logger = getVoidLogger(); +interface AnyJWK extends Record { + use: 'sig'; + alg: string; + kid: string; + kty: string; +} + +// Simplified copy of TokenFactory in @backstage/plugin-auth-backend +class FakeTokenFactory { + private readonly keys = new Array(); + + constructor( + private readonly options: { + issuer: string; + keyDurationSeconds: number; + }, + ) {} + + async issueToken(params: { + claims: { + sub: string; + ent?: string[]; + }; + }): Promise { + const key = await JWK.generate('EC', 'P-256', { + use: 'sig', + kid: uuid(), + alg: 'ES256', + }); + this.keys.push(key.toJWK(false) as unknown as AnyJWK); + + const iss = this.options.issuer; + const sub = params.claims.sub; + const ent = params.claims.ent; + const aud = 'backstage'; + const iat = Math.floor(Date.now() / 1000); + const exp = iat + this.options.keyDurationSeconds; + + return JWS.sign({ iss, sub, aud, iat, exp, ent }, key, { + alg: key.alg, + kid: key.kid, + }); + } + + async listPublicKeys(): Promise<{ keys: AnyJWK[] }> { + return { keys: this.keys }; + } +} function jwtKid(jwt: string): string { const { header } = JWT.decode(jwt, { complete: true }) as { @@ -48,8 +90,7 @@ const discovery: PluginEndpointDiscovery = { describe('IdentityClient', () => { let client: IdentityClient; - let factory: TokenFactory; - let keyStore: KeyStore; + let factory: FakeTokenFactory; const keyDurationSeconds = 5; beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); @@ -57,13 +98,10 @@ describe('IdentityClient', () => { afterEach(() => server.resetHandlers()); beforeEach(() => { - client = new IdentityClient({ discovery, issuer: mockBaseUrl }); - keyStore = new MemoryKeyStore(); - factory = new TokenFactory({ + client = IdentityClient.create({ discovery, issuer: mockBaseUrl }); + factory = new FakeTokenFactory({ issuer: mockBaseUrl, - keyStore: keyStore, keyDurationSeconds, - logger, }); }); @@ -83,7 +121,7 @@ describe('IdentityClient', () => { it('should use the correct endpoint', async () => { await factory.issueToken({ claims: { sub: 'foo' } }); const keys = await factory.listPublicKeys(); - const response = await client.listPublicKeys(); + const response = await (client as any).listPublicKeys(); expect(response).toEqual(keys); }); @@ -108,11 +146,9 @@ describe('IdentityClient', () => { }); it('should throw on incorrect issuer', async () => { - const hackerFactory = new TokenFactory({ + const hackerFactory = new FakeTokenFactory({ issuer: 'hacker', - keyStore, keyDurationSeconds, - logger, }); return expect(async () => { const token = await hackerFactory.issueToken({ @@ -137,11 +173,9 @@ describe('IdentityClient', () => { }); it('should throw on incorrect signing key', async () => { - const hackerFactory = new TokenFactory({ + const hackerFactory = new FakeTokenFactory({ issuer: mockBaseUrl, - keyStore: new MemoryKeyStore(), keyDurationSeconds, - logger, }); return expect(async () => { const token = await hackerFactory.issueToken({ @@ -199,38 +233,6 @@ describe('IdentityClient', () => { }); }); - describe('getBearerToken', () => { - it('should return undefined on undefined input', async () => { - const token = IdentityClient.getBearerToken(undefined); - expect(token).toBeUndefined(); - }); - - it('should return undefined on malformed input', async () => { - const token = IdentityClient.getBearerToken('malformed'); - expect(token).toBeUndefined(); - }); - - it('should return undefined on unexpected scheme', async () => { - const token = IdentityClient.getBearerToken('Basic token'); - expect(token).toBeUndefined(); - }); - - it('should return Bearer token', async () => { - const token = IdentityClient.getBearerToken('Bearer token'); - expect(token).toEqual('token'); - }); - - it('should return Bearer token despite extra space', async () => { - const token = IdentityClient.getBearerToken('Bearer \n token '); - expect(token).toEqual('token'); - }); - - it('should return Bearer token despite unconventionial case', async () => { - const token = IdentityClient.getBearerToken('bEARER token'); - expect(token).toEqual('token'); - }); - }); - describe('listPublicKeys', () => { const defaultServiceResponse: { keys: JSONWebKey[]; @@ -257,7 +259,7 @@ describe('IdentityClient', () => { }); it('should use the correct endpoint', async () => { - const response = await client.listPublicKeys(); + const response = await (client as any).listPublicKeys(); expect(response).toEqual(defaultServiceResponse); }); }); diff --git a/plugins/auth-backend/src/identity/IdentityClient.ts b/plugins/auth-node/src/IdentityClient.ts similarity index 85% rename from plugins/auth-backend/src/identity/IdentityClient.ts rename to plugins/auth-node/src/IdentityClient.ts index 552d231e1f..d8e841bf75 100644 --- a/plugins/auth-backend/src/identity/IdentityClient.ts +++ b/plugins/auth-node/src/IdentityClient.ts @@ -14,19 +14,20 @@ * limitations under the License. */ -import fetch from 'node-fetch'; -import { JWK, JWT, JWKS, JSONWebKey } from 'jose'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { AuthenticationError } from '@backstage/errors'; -import { BackstageIdentityResponse } from '../providers/types'; +import { JSONWebKey, JWK, JWKS, JWT } from 'jose'; +import fetch from 'node-fetch'; +import { BackstageIdentityResponse } from './types'; const CLOCK_MARGIN_S = 10; /** - * A identity client to interact with auth-backend - * and authenticate backstage identity tokens + * An identity client to interact with auth-backend and authenticate Backstage + * tokens * * @experimental This is not a stable API yet + * @public */ export class IdentityClient { private readonly discovery: PluginEndpointDiscovery; @@ -34,7 +35,20 @@ export class IdentityClient { private keyStore: JWKS.KeyStore; private keyStoreUpdated: number; - constructor(options: { discovery: PluginEndpointDiscovery; issuer: string }) { + /** + * Create a new {@link IdentityClient} instance. + */ + static create(options: { + discovery: PluginEndpointDiscovery; + issuer: string; + }): IdentityClient { + return new IdentityClient(options); + } + + private constructor(options: { + discovery: PluginEndpointDiscovery; + issuer: string; + }) { this.discovery = options.discovery; this.issuer = options.issuer; this.keyStore = new JWKS.KeyStore(); @@ -84,20 +98,6 @@ export class IdentityClient { return user; } - /** - * Parses the given authorization header and returns - * the bearer token, or null if no bearer token is given - */ - static getBearerToken( - authorizationHeader: string | undefined, - ): string | undefined { - if (typeof authorizationHeader !== 'string') { - return undefined; - } - const matches = authorizationHeader.match(/Bearer\s+(\S+)/i); - return matches?.[1]; - } - /** * Returns the public signing key matching the given jwt token, * or null if no matching key was found @@ -125,7 +125,7 @@ export class IdentityClient { /** * Lists public part of keys used to sign Backstage Identity tokens */ - async listPublicKeys(): Promise<{ + private async listPublicKeys(): Promise<{ keys: JSONWebKey[]; }> { const url = `${await this.discovery.getBaseUrl( diff --git a/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts new file mode 100644 index 0000000000..0bd7b5ea93 --- /dev/null +++ b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; + +describe('getBearerToken', () => { + it('should return undefined on bad input', async () => { + expect(getBearerTokenFromAuthorizationHeader(undefined)).toBeUndefined(); + expect(getBearerTokenFromAuthorizationHeader(7)).toBeUndefined(); + expect( + getBearerTokenFromAuthorizationHeader('Bearer \n token'), + ).toBeUndefined(); + expect( + getBearerTokenFromAuthorizationHeader('Bearer token '), + ).toBeUndefined(); + }); + + it('should return undefined on malformed input', async () => { + const token = getBearerTokenFromAuthorizationHeader('malformed'); + expect(token).toBeUndefined(); + }); + + it('should return undefined on unexpected scheme', async () => { + const token = getBearerTokenFromAuthorizationHeader('Basic token'); + expect(token).toBeUndefined(); + }); + + it('should return Bearer token', async () => { + const token = getBearerTokenFromAuthorizationHeader('Bearer token'); + expect(token).toEqual('token'); + }); + + it('should return Bearer token despite unconventional case', async () => { + const token = getBearerTokenFromAuthorizationHeader('bEARER token'); + expect(token).toEqual('token'); + }); +}); diff --git a/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts new file mode 100644 index 0000000000..8451cd6ab1 --- /dev/null +++ b/plugins/auth-node/src/getBearerTokenFromAuthorizationHeader.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Parses the given authorization header and returns the bearer token, or + * undefined if no bearer token is given. + * + * @remarks + * + * This function is explicitly built to tolerate bad inputs safely, so you may + * call it directly with e.g. the output of `req.header('authorization')` + * without first checking that it exists. + * + * @public + */ +export function getBearerTokenFromAuthorizationHeader( + authorizationHeader: unknown, +): string | undefined { + if (typeof authorizationHeader !== 'string') { + return undefined; + } + const matches = authorizationHeader.match(/^Bearer[ ]+(\S+)$/i); + return matches?.[1]; +} diff --git a/plugins/auth-node/src/index.ts b/plugins/auth-node/src/index.ts new file mode 100644 index 0000000000..5551f2c85d --- /dev/null +++ b/plugins/auth-node/src/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common functionality and types for the Backstage auth plugin. + * + * @packageDocumentation + */ + +export { getBearerTokenFromAuthorizationHeader } from './getBearerTokenFromAuthorizationHeader'; +export { IdentityClient } from './IdentityClient'; +export type { + BackstageIdentityResponse, + BackstageSignInResult, + BackstageUserIdentity, +} from './types'; diff --git a/plugins/auth-node/src/setupTests.ts b/plugins/auth-node/src/setupTests.ts new file mode 100644 index 0000000000..d3232290a7 --- /dev/null +++ b/plugins/auth-node/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/auth-node/src/types.ts b/plugins/auth-node/src/types.ts new file mode 100644 index 0000000000..b574c2204d --- /dev/null +++ b/plugins/auth-node/src/types.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; + +/** + * A representation of a successful Backstage sign-in. + * + * Compared to the {@link BackstageIdentityResponse} this type omits + * the decoded identity information embedded in the token. + * + * @public + */ +export interface BackstageSignInResult { + /** + * An opaque ID that uniquely identifies the user within Backstage. + * + * This is typically the same as the user entity `metadata.name`. + * + * @deprecated Use the `identity` field instead + */ + id: string; + + /** + * The entity that the user is represented by within Backstage. + * + * This entity may or may not exist within the Catalog, and it can be used + * to read and store additional metadata about the user. + * + * @deprecated Use the `identity` field instead. + */ + entity?: Entity; + + /** + * The token used to authenticate the user within Backstage. + */ + token: string; +} + +/** + * Response object containing the {@link BackstageUserIdentity} and the token + * from the authentication provider. + * + * @public + */ +export interface BackstageIdentityResponse extends BackstageSignInResult { + /** + * A plaintext description of the identity that is encapsulated within the token. + */ + identity: BackstageUserIdentity; +} + +/** + * User identity information within Backstage. + * + * @public + */ +export type BackstageUserIdentity = { + /** + * The type of identity that this structure represents. In the frontend app + * this will currently always be 'user'. + */ + type: 'user'; + + /** + * The entityRef of the user in the catalog. + * For example User:default/sandra + */ + userEntityRef: string; + + /** + * The user and group entities that the user claims ownership through + */ + ownershipEntityRefs: string[]; +}; diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index 16b4a0d182..0b44be4e13 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-azure-devops-backend +## 0.3.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + +## 0.3.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.3.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.3.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + ## 0.3.1 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index d0bcca2db9..a87a48046e 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.3.1", + "version": "0.3.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config": "^0.1.13", "@backstage/plugin-azure-devops-common": "^0.2.0", "@types/express": "^4.17.6", @@ -32,7 +32,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", "msw": "^0.35.0" diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index 0b9b3ad3d6..02faebca61 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "devDependencies": { - "@backstage/cli": "^0.13.0" + "@backstage/cli": "^0.13.2-next.0" }, "files": [ "dist" diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 4076ca6fc0..8b0331e75d 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-azure-devops +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.1.12 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index bc92e208d3..a34686d856 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.1.12", + "version": "0.1.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,11 +28,11 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", "@backstage/plugin-azure-devops-common": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,16 +46,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index 4f54700697..f025515703 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-badges-backend +## 0.1.18 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/backend-common@0.10.7 + +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + ## 0.1.16 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index a2c74623f1..b5fc38f289 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.1.16", + "version": "0.1.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/catalog-client": "^0.5.5", + "@backstage/backend-common": "^0.10.7", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", @@ -45,7 +45,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index 32226f40db..8f2a3b2e84 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-badges +## 0.2.22 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.2.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.2.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.2.20 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 3a21138d70..5250318164 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.20", + "version": "0.2.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,10 +28,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,16 +43,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index ab4bd8d674..fdd63ff59b 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-bazaar-backend +## 0.1.9 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7 + - @backstage/backend-test-utils@0.1.17 + +## 0.1.9-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + - @backstage/backend-test-utils@0.1.17-next.0 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + - @backstage/backend-test-utils@0.1.16 + +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + - @backstage/backend-test-utils@0.1.16-next.1 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.16-next.0 + ## 0.1.7 ### Patch Changes diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 14f2332eb2..71980acea4 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.1.7", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,18 +20,18 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/backend-test-utils": "^0.1.15", + "@backstage/backend-common": "^0.10.7", + "@backstage/backend-test-utils": "^0.1.17", "@backstage/config": "^0.1.13", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "knex": "^0.95.1", + "knex": "^1.0.2", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0" + "@backstage/cli": "^0.13.2" }, "files": [ "dist", diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index ea92aa9f27..00f7605888 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-bazaar +## 0.1.13 + +### Patch Changes + +- d674971d3a: Rolling back the `@date-io/luxon` bump as this broke both packages, and we need it for `@material-ui/pickers` +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/cli@0.13.2 + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + - @backstage/plugin-catalog@0.7.12 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.13.2-next.0 + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + - @backstage/plugin-catalog@0.7.12-next.0 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + - @backstage/cli@0.13.1 + - @backstage/plugin-catalog@0.7.11 + +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/cli@0.13.1-next.0 + - @backstage/plugin-catalog@0.7.11-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.1.11 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 40ea514392..bfdcb18ca3 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.1.11", + "version": "0.1.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,14 +21,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.5.5", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", - "@backstage/cli": "^0.13.0", - "@backstage/core-components": "^0.8.6", + "@backstage/cli": "^0.13.2", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog": "^0.7.10", - "@backstage/plugin-catalog-react": "^0.6.12", - "@date-io/luxon": "2.x", + "@backstage/plugin-catalog": "^0.7.12", + "@backstage/plugin-catalog-react": "^0.6.14", + "@date-io/luxon": "1.x", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -44,10 +44,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/dev-utils": "^0.2.19", + "@backstage/cli": "^0.13.2", + "@backstage/dev-utils": "^0.2.21", "@testing-library/jest-dom": "^5.10.1", - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" }, "files": [ "dist" diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index a62137d1e1..3fc26f7da7 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-bitrise +## 0.1.25 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.1.24 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.1.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.1.23 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 87f377632b..8b50459a84 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.23", + "version": "0.1.25", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,14 +22,14 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "lodash": "^4.17.21", "luxon": "^2.0.2", "qs": "^6.9.6", @@ -40,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 4495964ca8..55162c6003 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.3.12 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.3 + +## 0.3.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.3-next.0 + +## 0.3.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.2 + +## 0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.2-next.0 + ## 0.3.10 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 1b43e66b47..a499049265 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend modules that helps integrate towards LDAP", - "version": "0.3.10", + "version": "0.3.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-backend": "^0.21.1", + "@backstage/plugin-catalog-backend": "^0.21.3", "@backstage/types": "^0.1.1", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", @@ -40,7 +40,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/lodash": "^4.14.151" }, "files": [ diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts index c74e5b3aee..4c5bd2e3bd 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts @@ -15,9 +15,9 @@ */ import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, Entity, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -207,8 +207,8 @@ function withLocations(providerId: string, entity: Entity): Entity { { metadata: { annotations: { - [LOCATION_ANNOTATION]: location, - [ORIGIN_LOCATION_ANNOTATION]: location, + [ANNOTATION_LOCATION]: location, + [ANNOTATION_ORIGIN_LOCATION]: location, }, }, }, diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index bd04fbcef7..847fbae9d4 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.15 + +### Patch Changes + +- 9b122a780c: Add userExpand option to allow users to expand fields retrieved from the Graph API - for use in custom transformers +- 7bb1bde7f6: Minor API cleanups +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.3 + +## 0.2.15-next.0 + +### Patch Changes + +- 9b122a780c: Add userExpand option to allow users to expand fields retrieved from the Graph API - for use in custom transformers +- 7bb1bde7f6: Minor API cleanups +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.3-next.0 + +## 0.2.14 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.2 + +## 0.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.2-next.0 + ## 0.2.13 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 662056eff1..4f5d247a06 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -33,6 +33,15 @@ export function defaultUserTransformer( userPhoto?: string, ): Promise; +// @public +export type GroupMember = + | (MicrosoftGraph.Group & { + '@odata.type': '#microsoft.graph.user'; + }) + | (MicrosoftGraph.User & { + '@odata.type': '#microsoft.graph.group'; + }); + // @public export type GroupTransformer = ( group: MicrosoftGraph.Group, @@ -54,7 +63,6 @@ export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id'; export class MicrosoftGraphClient { constructor(baseUrl: string, pca: msal.ConfidentialClientApplication); static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient; - // Warning: (ae-forgotten-export) The symbol "GroupMember" needs to be exported by the entry point index.d.ts getGroupMembers(groupId: string): AsyncIterable; // (undocumented) getGroupPhoto(groupId: string, sizeId?: string): Promise; @@ -141,6 +149,7 @@ export type MicrosoftGraphProviderConfig = { clientId: string; clientSecret: string; userFilter?: string; + userExpand?: string[]; userGroupMemberFilter?: string; groupFilter?: string; }; @@ -170,6 +179,7 @@ export function readMicrosoftGraphOrg( client: MicrosoftGraphClient, tenantId: string, options: { + userExpand?: string[]; userFilter?: string; userGroupMemberFilter?: string; groupFilter?: string; diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 09d55c42e9..7fd0a2d319 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend modules that helps integrate towards Microsoft Graph", - "version": "0.2.13", + "version": "0.2.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,19 +32,19 @@ "@azure/msal-node": "^1.1.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", - "@backstage/plugin-catalog-backend": "^0.21.1", + "@backstage/plugin-catalog-backend": "^0.21.3", "@microsoft/microsoft-graph-types": "^2.6.0", "@types/node-fetch": "^2.5.12", "lodash": "^4.17.21", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "p-limit": "^3.0.2", "winston": "^3.2.1", "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/backend-common": "^0.10.7", + "@backstage/cli": "^0.13.2", + "@backstage/test-utils": "^0.2.4", "@types/lodash": "^4.14.151", "msw": "^0.35.0" }, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index 4d37e24632..c401bc2553 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -52,6 +52,12 @@ export type MicrosoftGraphProviderConfig = { * E.g. "accountEnabled eq true and userType eq 'member'" */ userFilter?: string; + /** + * The expand argument to apply to users. + * + * E.g. "manager" + */ + userExpand?: string[]; /** * The filter to apply to extract users by groups memberships. * diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts index b0d53b43a2..61e62f5803 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { MicrosoftGraphClient } from './client'; -export type { ODataQuery } from './client'; +export type { GroupMember, ODataQuery } from './client'; export { readMicrosoftGraphConfig } from './config'; export type { MicrosoftGraphProviderConfig } from './config'; export { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index b2845ac78d..58e60ab76a 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -85,6 +85,7 @@ export async function readMicrosoftGraphUsers( client: MicrosoftGraphClient, options: { userFilter?: string; + userExpand?: string[]; transformer?: UserTransformer; logger: Logger; }, @@ -99,6 +100,7 @@ export async function readMicrosoftGraphUsers( for await (const user of client.getUsers({ filter: options.userFilter, + expand: options.userExpand, })) { // Process all users in parallel, otherwise it can take quite some time promises.push( @@ -500,6 +502,7 @@ export async function readMicrosoftGraphOrg( client: MicrosoftGraphClient, tenantId: string, options: { + userExpand?: string[]; userFilter?: string; userGroupMemberFilter?: string; groupFilter?: string; @@ -524,6 +527,7 @@ export async function readMicrosoftGraphOrg( } else { const { users: usersWithFilter } = await readMicrosoftGraphUsers(client, { userFilter: options.userFilter, + userExpand: options.userExpand, transformer: options.userTransformer, logger: options.logger, }); diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts index eab2d93526..456afda550 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.test.ts @@ -15,9 +15,9 @@ */ import { getVoidLogger } from '@backstage/backend-common'; import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, GroupEntity, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, UserEntity, } from '@backstage/catalog-model'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; @@ -162,8 +162,8 @@ describe('withLocations', () => { name: 'u1', annotations: { [MICROSOFT_GRAPH_USER_ID_ANNOTATION]: 'uid', - [LOCATION_ANNOTATION]: 'msgraph:test/uid', - [ORIGIN_LOCATION_ANNOTATION]: 'msgraph:test/uid', + [ANNOTATION_LOCATION]: 'msgraph:test/uid', + [ANNOTATION_ORIGIN_LOCATION]: 'msgraph:test/uid', }, }, spec: { diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts index 6d66723970..92836893f0 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts @@ -15,9 +15,9 @@ */ import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, Entity, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -180,8 +180,8 @@ export function withLocations(providerId: string, entity: Entity): Entity { { metadata: { annotations: { - [LOCATION_ANNOTATION]: location, - [ORIGIN_LOCATION_ANNOTATION]: location, + [ANNOTATION_LOCATION]: location, + [ANNOTATION_ORIGIN_LOCATION]: location, }, }, }, diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 351a4983d9..e0e63d86fc 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -102,6 +102,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { client, provider.tenantId, { + userExpand: provider.userExpand, userFilter: provider.userFilter, userGroupMemberFilter: provider.userGroupMemberFilter, groupFilter: provider.groupFilter, diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 523fcd0bd1..a371712c80 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-catalog-backend +## 0.21.3 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/backend-common@0.10.7 + - @backstage/plugin-permission-node@0.4.3 + +## 0.21.3-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + - @backstage/plugin-permission-node@0.4.3-next.0 + +## 0.21.2 + +### Patch Changes + +- fac5f112b4: chore(deps): bump `prom-client` from 13.2.0 to 14.0.1 +- 5bbffa60be: Pass authorization token to location service inside location api routes +- Updated dependencies + - @backstage/plugin-catalog-common@0.1.2 + - @backstage/backend-common@0.10.6 + - @backstage/plugin-permission-node@0.4.2 + +## 0.21.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@0.1.2-next.0 + - @backstage/backend-common@0.10.6-next.0 + - @backstage/plugin-permission-node@0.4.2-next.1 + +## 0.21.2-next.0 + +### Patch Changes + +- fac5f112b4: chore(deps): bump `prom-client` from 13.2.0 to 14.0.1 +- 5bbffa60be: Pass authorization token to location service inside location api routes +- Updated dependencies + - @backstage/plugin-permission-node@0.4.2-next.0 + ## 0.21.1 ### Patch Changes diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index deed3e2d8b..d97df9967c 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -8,7 +8,6 @@ import { Account } from 'aws-sdk/clients/organizations'; import { BitbucketIntegration } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; -import { CatalogEntitiesRequest } from '@backstage/catalog-client'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-node'; import { Conditions } from '@backstage/plugin-permission-node'; import { Config } from '@backstage/config'; @@ -17,12 +16,13 @@ import { Entity } from '@backstage/catalog-model'; import { EntityPolicy } from '@backstage/catalog-model'; import { EntityRelationSpec } from '@backstage/catalog-model'; import express from 'express'; +import { GetEntitiesRequest } from '@backstage/catalog-client'; import { GithubCredentialsProvider } from '@backstage/integration'; import { GitHubIntegrationConfig } from '@backstage/integration'; import { IndexableDocument } from '@backstage/search-common'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; -import { Location as Location_2 } from '@backstage/catalog-model'; +import { Location as Location_2 } from '@backstage/catalog-client'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; import { Organizations } from 'aws-sdk'; @@ -36,7 +36,6 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { ResourceEntityV1alpha1 } from '@backstage/catalog-model'; import { Router } from 'express'; import { ScmIntegrationRegistry } from '@backstage/integration'; -import { ScmIntegrations } from '@backstage/integration'; import { TokenManager } from '@backstage/backend-common'; import { UrlReader } from '@backstage/backend-common'; import { Validators } from '@backstage/catalog-model'; @@ -177,7 +176,10 @@ export class AwsS3DiscoveryProcessor implements CatalogProcessor { // // @public export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { - constructor(options: { integrations: ScmIntegrations; logger: Logger_2 }); + constructor(options: { + integrations: ScmIntegrationRegistry; + logger: Logger_2; + }); // (undocumented) static fromConfig( config: Config, @@ -464,7 +466,7 @@ export type CatalogRulesEnforcer = { // @public (undocumented) export class CodeOwnersProcessor implements CatalogProcessor { constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger_2; reader: UrlReader; }); @@ -509,7 +511,7 @@ export class DefaultCatalogCollator implements DocumentCollator { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; locationTemplate?: string; - filter?: CatalogEntitiesRequest['filter']; + filter?: GetEntitiesRequest['filter']; catalogClient?: CatalogApi; }); // (undocumented) @@ -524,14 +526,14 @@ export class DefaultCatalogCollator implements DocumentCollator { // (undocumented) execute(): Promise; // (undocumented) - protected filter?: CatalogEntitiesRequest['filter']; + protected filter?: GetEntitiesRequest['filter']; // (undocumented) static fromConfig( _config: Config, options: { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; - filter?: CatalogEntitiesRequest['filter']; + filter?: GetEntitiesRequest['filter']; }, ): DefaultCatalogCollator; // (undocumented) @@ -750,7 +752,7 @@ function generalError( // @public export class GithubDiscoveryProcessor implements CatalogProcessor { constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger_2; githubCredentialsProvider?: GithubCredentialsProvider; }); @@ -773,7 +775,7 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { // @alpha export class GithubMultiOrgReaderProcessor implements CatalogProcessor { constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger_2; orgs: GithubMultiOrgConfig; githubCredentialsProvider?: GithubCredentialsProvider; @@ -828,7 +830,7 @@ export class GitHubOrgEntityProvider implements EntityProvider { // @public export class GithubOrgReaderProcessor implements CatalogProcessor { constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger_2; githubCredentialsProvider?: GithubCredentialsProvider; }); diff --git a/plugins/catalog-backend/migrations/20200807120600_entitySearch.js b/plugins/catalog-backend/migrations/20200807120600_entitySearch.js index bdb6037d65..aa05e79be9 100644 --- a/plugins/catalog-backend/migrations/20200807120600_entitySearch.js +++ b/plugins/catalog-backend/migrations/20200807120600_entitySearch.js @@ -23,7 +23,7 @@ exports.up = async function up(knex) { // Sqlite does not support alter column. if (knex.client.config.client !== 'sqlite3') { await knex.schema.alterTable('entities_search', table => { - table.text('value').nullable().alter(); + table.text('value').nullable().alter({ alterType: true }); }); } }; @@ -35,7 +35,7 @@ exports.down = async function down(knex) { // Sqlite does not support alter column. if (knex.client.config.client !== 'sqlite3') { await knex.schema.alterTable('entities_search', table => { - table.string('value').nullable().alter(); + table.string('value').nullable().alter({ alterType: true }); }); } }; diff --git a/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js index 2f9b2821eb..366a4b7044 100644 --- a/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js +++ b/plugins/catalog-backend/migrations/20201005122705_add_entity_full_name.js @@ -33,7 +33,7 @@ exports.up = async function up(knex) { // SQLite does not support alter column if (knex.client.config.client !== 'sqlite3') { await knex.schema.alterTable('entities', table => { - table.text('full_name').notNullable().alter(); + table.text('full_name').notNullable().alter({ alterNullable: true }); }); } diff --git a/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js b/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js index 214a8f4a73..35b0474f06 100644 --- a/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js +++ b/plugins/catalog-backend/migrations/20201006130744_entity_data_column.js @@ -43,7 +43,7 @@ exports.up = async function up(knex) { // SQLite does not support ALTER COLUMN. if (knex.client.config.client !== 'sqlite3') { await knex.schema.alterTable('entities', table => { - table.text('data').notNullable().alter(); + table.text('data').notNullable().alter({ alterNullable: true }); }); } }; diff --git a/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js b/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js index c4413d4563..9a5ccce9fd 100644 --- a/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js +++ b/plugins/catalog-backend/migrations/20201230103504_update_log_varchar.js @@ -26,8 +26,8 @@ exports.up = async function up(knex) { await knex.schema .raw('DROP VIEW location_update_log_latest;') .alterTable('location_update_log', table => { - table.text('message').alter(); - table.text('entity_name').nullable().alter(); + table.text('message').alter({ alterType: true }); + table.text('entity_name').nullable().alter({ alterType: true }); }).raw(` CREATE VIEW location_update_log_latest AS SELECT t1.* FROM location_update_log t1 @@ -53,8 +53,8 @@ exports.down = async function down(knex) { await knex.schema .raw('DROP VIEW location_update_log_latest;') .alterTable('location_update_log', table => { - table.string('message').alter(); - table.string('entity_name').nullable().alter(); + table.string('message').alter({ alterType: true }); + table.string('entity_name').nullable().alter({ alterType: true }); }).raw(` CREATE VIEW location_update_log_latest AS SELECT t1.* FROM location_update_log t1 diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 748ff77592..a4a40af206 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "0.21.1", + "version": "0.21.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,15 +30,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/catalog-client": "^0.5.5", + "@backstage/backend-common": "^0.10.7", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", "@backstage/integration": "^0.7.2", - "@backstage/plugin-catalog-common": "^0.1.1", + "@backstage/plugin-catalog-common": "^0.1.2", "@backstage/plugin-permission-common": "^0.4.0", - "@backstage/plugin-permission-node": "^0.4.1", + "@backstage/plugin-permission-node": "^0.4.3", "@backstage/search-common": "^0.2.2", "@backstage/types": "^0.1.1", "@octokit/graphql": "^4.5.8", @@ -52,32 +52,31 @@ "fs-extra": "9.1.0", "git-url-parse": "^11.6.0", "glob": "^7.1.6", - "knex": "^0.95.1", + "knex": "^1.0.2", "lodash": "^4.17.21", "luxon": "^2.0.2", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "p-limit": "^3.0.2", "prom-client": "^14.0.1", "uuid": "^8.0.0", "winston": "^3.2.1", "yaml": "^1.9.2", "yn": "^4.0.0", - "yup": "^0.32.9" + "zod": "^3.11.6" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.15", - "@backstage/cli": "^0.13.0", + "@backstage/backend-test-utils": "^0.1.17", + "@backstage/cli": "^0.13.2", "@backstage/plugin-permission-common": "^0.4.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/test-utils": "^0.2.4", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", - "@types/yup": "^0.29.13", + "@vscode/sqlite3": "^5.0.7", "aws-sdk-mock": "^5.2.1", "msw": "^0.35.0", - "sqlite3": "^5.0.1", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2", "luxon": "^2.0.2" diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts index 905339af17..729be442aa 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.test.ts @@ -832,7 +832,7 @@ describe('Default Processing Database', () => { /* config-1 -> location:default/root config-2 -> location:default/root - */ + */ await createLocations(knex, ['location:default/root']); await insertRefRow(knex, { @@ -1092,6 +1092,61 @@ describe('Default Processing Database', () => { }, 60_000, ); + + it.each(databases.eachSupportedId())( + 'should successfully fall back from batch to individual mode on conflicts, %p', + async databaseId => { + const fakeLogger = { + debug: jest.fn(), + }; + const { knex, db } = await createDatabase( + databaseId, + fakeLogger as any, + ); + + await createLocations(knex, ['component:default/a']); + + await insertRefRow(knex, { + source_key: undefined, + target_entity_ref: 'component:default/a', + }); + + await db.transaction(async tx => { + await db.replaceUnprocessedEntities(tx, { + type: 'full', + sourceKey: 'lols', + items: [ + { + entity: { + apiVersion: '1', + kind: 'Component', + metadata: { name: 'a' }, + spec: { marker: 'WILL_CHANGE' }, + } as Entity, + locationKey: 'file:///tmp/a', + }, + ], + }); + }); + expect(fakeLogger.debug).toBeCalledWith( + expect.stringMatching( + /Fast insert path failed, falling back to slow path/, + ), + ); + + const state = await knex('refresh_state').select(); + expect(state).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + entity_ref: 'component:default/a', + location_key: 'file:///tmp/a', + unprocessed_entity: expect.stringContaining('WILL_CHANGE'), + }), + ]), + ); + }, + 60_000, + ); }); describe('getProcessableEntities', () => { diff --git a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts index f2f478c481..55f04c289c 100644 --- a/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/database/DefaultProcessingDatabase.ts @@ -45,6 +45,7 @@ import { } from './tables'; import { generateStableHash } from './util'; +import { isDatabaseConflictError } from '@backstage/backend-common'; // The number of items that are sent per batch to the database layer, when // doing .batchInsert calls to knex. This needs to be low enough to not cause @@ -160,7 +161,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { ): Promise { const tx = txOpaque as Knex.Transaction; - const { toUpsert, toRemove } = await this.createDelta(tx, options); + const { toAdd, toUpsert, toRemove } = await this.createDelta(tx, options); if (toRemove.length) { // TODO(freben): Batch split, to not hit variable limits? @@ -277,6 +278,53 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { ); } + if (toAdd.length) { + // The reason for this chunking, rather than just massively batch + // inserting the entire payload, is that we fall back to the individual + // upsert mechanism below on conflicts. That path is massively slower than + // the fast batch path, so we don't want to end up accidentally having to + // for example item-by-item upsert tens of thousands of entities in a + // large initial delivery dump. The implication is that the size of these + // chunks needs to weigh the benefit of fast successful inserts, against + // the drawback of super slow but more rare fallbacks. There's quickly + // diminishing returns though with turning up this value way high. + for (const chunk of lodash.chunk(toAdd, 50)) { + try { + await tx.batchInsert( + 'refresh_state', + chunk.map(item => ({ + entity_id: uuid(), + entity_ref: stringifyEntityRef(item.deferred.entity), + unprocessed_entity: JSON.stringify(item.deferred.entity), + unprocessed_hash: item.hash, + errors: '', + location_key: item.deferred.locationKey, + next_update_at: tx.fn.now(), + last_discovery_at: tx.fn.now(), + })), + BATCH_SIZE, + ); + await tx.batchInsert( + 'refresh_state_references', + chunk.map(item => ({ + source_key: options.sourceKey, + target_entity_ref: stringifyEntityRef(item.deferred.entity), + })), + BATCH_SIZE, + ); + } catch (error) { + if (!isDatabaseConflictError(error)) { + throw error; + } else { + this.options.logger.debug( + `Fast insert path failed, falling back to slow path, ${error}`, + ); + toUpsert.push(...chunk); + } + } + } + } + if (toUpsert.length) { for (const { deferred: { entity, locationKey }, @@ -600,11 +648,13 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { tx: Knex.Transaction, options: ReplaceUnprocessedEntitiesOptions, ): Promise<{ + toAdd: { deferred: DeferredEntity; hash: string }[]; toUpsert: { deferred: DeferredEntity; hash: string }[]; toRemove: string[]; }> { if (options.type === 'delta') { return { + toAdd: [], toUpsert: options.added.map(e => ({ deferred: e, hash: generateStableHash(e.entity), @@ -644,6 +694,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { ); const newRefsSet = new Set(items.map(item => item.ref)); + const toAdd = new Array<{ deferred: DeferredEntity; hash: string }>(); const toUpsert = new Array<{ deferred: DeferredEntity; hash: string }>(); const toRemove = oldRefs .map(row => row.target_entity_ref) @@ -654,18 +705,18 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { const upsertItem = { deferred: item.deferred, hash: item.hash }; if (!oldRef) { // Add any entity that does not exist in the database - toUpsert.push(upsertItem); + toAdd.push(upsertItem); } else if (oldRef.locationKey !== item.deferred.locationKey) { // Remove and then re-add any entity that exists, but with a different location key toRemove.push(item.ref); - toUpsert.push(upsertItem); + toAdd.push(upsertItem); } else if (oldRef.oldEntityHash !== item.hash) { // Entities with modifications should be pushed through too toUpsert.push(upsertItem); } } - return { toUpsert, toRemove }; + return { toAdd, toUpsert, toRemove }; } /** diff --git a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts index 54079e13ff..8adbaec628 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AnnotateLocationEntityProcessor.ts @@ -15,14 +15,14 @@ */ import { - EDIT_URL_ANNOTATION, + ANNOTATION_EDIT_URL, + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, + ANNOTATION_SOURCE_LOCATION, + ANNOTATION_VIEW_URL, Entity, LocationSpec, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, - SOURCE_LOCATION_ANNOTATION, - stringifyLocationReference, - VIEW_URL_ANNOTATION, + stringifyLocationRef, } from '@backstage/catalog-model'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { identity, merge, pickBy } from 'lodash'; @@ -58,7 +58,7 @@ export class AnnotateLocationEntityProcessor implements CatalogProcessor { }); if (sourceUrl) { - sourceLocation = stringifyLocationReference({ + sourceLocation = stringifyLocationRef({ type: 'url', target: sourceUrl, }); @@ -70,12 +70,12 @@ export class AnnotateLocationEntityProcessor implements CatalogProcessor { metadata: { annotations: pickBy( { - [LOCATION_ANNOTATION]: stringifyLocationReference(location), - [ORIGIN_LOCATION_ANNOTATION]: - stringifyLocationReference(originLocation), - [VIEW_URL_ANNOTATION]: viewUrl, - [EDIT_URL_ANNOTATION]: editUrl, - [SOURCE_LOCATION_ANNOTATION]: sourceLocation, + [ANNOTATION_LOCATION]: stringifyLocationRef(location), + [ANNOTATION_ORIGIN_LOCATION]: + stringifyLocationRef(originLocation), + [ANNOTATION_VIEW_URL]: viewUrl, + [ANNOTATION_EDIT_URL]: editUrl, + [ANNOTATION_SOURCE_LOCATION]: sourceLocation, }, identity, ), diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts index aeac64f54d..4f5df6f656 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts @@ -16,7 +16,10 @@ import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { ScmIntegrations } from '@backstage/integration'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; import { Logger } from 'winston'; import * as results from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; @@ -37,7 +40,7 @@ import { codeSearch } from './azure'; * target: https://dev.azure.com/org/project/_git/repo **/ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { - private readonly integrations: ScmIntegrations; + private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; static fromConfig(config: Config, options: { logger: Logger }) { @@ -49,7 +52,10 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { }); } - constructor(options: { integrations: ScmIntegrations; logger: Logger }) { + constructor(options: { + integrations: ScmIntegrationRegistry; + logger: Logger; + }) { this.integrations = options.integrations; this.logger = options.logger; } diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts index 33beffed0d..c42980d97b 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.test.ts @@ -251,13 +251,14 @@ describe('BuiltinKindsEntityProcessor', () => { type: 'database', owner: 'o', dependsOn: ['Component:c', 'Resource:r'], + dependencyOf: ['Component:d'], system: 's', }, }; await processor.postProcessEntity(entity, location, emit); - expect(emit).toBeCalledTimes(8); + expect(emit).toBeCalledTimes(10); expect(emit).toBeCalledWith({ type: 'relation', relation: { @@ -325,6 +326,23 @@ describe('BuiltinKindsEntityProcessor', () => { target: { kind: 'System', namespace: 'default', name: 's' }, }, }); + + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Resource', namespace: 'default', name: 'n' }, + type: 'dependencyOf', + target: { kind: 'Component', namespace: 'default', name: 'd' }, + }, + }); + expect(emit).toBeCalledWith({ + type: 'relation', + relation: { + source: { kind: 'Component', namespace: 'default', name: 'd' }, + type: 'dependsOn', + target: { kind: 'Resource', namespace: 'default', name: 'n' }, + }, + }); }); it('generates an error for resource entities with unspecified dependsOn entity reference kinds', async () => { diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts index bcabc00f32..5999be9793 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts @@ -225,6 +225,12 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { RELATION_DEPENDS_ON, RELATION_DEPENDENCY_OF, ); + doEmit( + resource.spec.dependencyOf, + { defaultNamespace: selfRef.namespace }, + RELATION_DEPENDENCY_OF, + RELATION_DEPENDS_ON, + ); doEmit( resource.spec.system, { defaultKind: 'System', defaultNamespace: selfRef.namespace }, diff --git a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts index a70aa6d1a9..f358abf7e9 100644 --- a/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/CodeOwnersProcessor.ts @@ -17,7 +17,10 @@ import { UrlReader } from '@backstage/backend-common'; import { Entity, LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { ScmIntegrations } from '@backstage/integration'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; import { Logger } from 'winston'; import { findCodeOwnerByTarget } from './codeowners'; import { CatalogProcessor } from './types'; @@ -35,7 +38,7 @@ const ALLOWED_LOCATION_TYPES = [ ]; export class CodeOwnersProcessor implements CatalogProcessor { - private readonly integrations: ScmIntegrations; + private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; private readonly reader: UrlReader; @@ -52,7 +55,7 @@ export class CodeOwnersProcessor implements CatalogProcessor { } constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger; reader: UrlReader; }) { diff --git a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts index fabf3a1136..cc0113bfbe 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts @@ -16,7 +16,10 @@ import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { ScmIntegrations } from '@backstage/integration'; +import { + ScmIntegrationRegistry, + ScmIntegrations, +} from '@backstage/integration'; import { Logger } from 'winston'; import * as results from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; @@ -31,7 +34,7 @@ import { * Extracts repositories out of an GitLab instance. */ export class GitLabDiscoveryProcessor implements CatalogProcessor { - private readonly integrations: ScmIntegrations; + private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; private readonly cache: CacheClient; @@ -48,7 +51,7 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { } private constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; pluginCache: PluginCacheManager; logger: Logger; }) { diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts index 3196c4e161..3cb73f0181 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts @@ -19,6 +19,7 @@ import { Config } from '@backstage/config'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, + ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; @@ -42,7 +43,7 @@ import { CatalogProcessor, CatalogProcessorEmit } from './types'; * target: https://github.com/backstage/*\/blob/main/catalog-info.yaml **/ export class GithubDiscoveryProcessor implements CatalogProcessor { - private readonly integrations: ScmIntegrations; + private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; private readonly githubCredentialsProvider: GithubCredentialsProvider; @@ -62,7 +63,7 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { } constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }) { diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts index dd705ad691..7d10428494 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubMultiOrgReaderProcessor.ts @@ -21,6 +21,7 @@ import { GithubAppCredentialsMux, GithubCredentialsProvider, GitHubIntegrationConfig, + ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; import { graphql } from '@octokit/graphql'; @@ -42,7 +43,7 @@ import { buildOrgHierarchy } from './util/org'; * Be aware that this processor may not be compatible with future org structures in the catalog. */ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { - private readonly integrations: ScmIntegrations; + private readonly integrations: ScmIntegrationRegistry; private readonly orgs: GithubMultiOrgConfig; private readonly logger: Logger; private readonly githubCredentialsProvider: GithubCredentialsProvider; @@ -65,7 +66,7 @@ export class GithubMultiOrgReaderProcessor implements CatalogProcessor { } constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger; orgs: GithubMultiOrgConfig; githubCredentialsProvider?: GithubCredentialsProvider; diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts index 474d08feee..dbb7b98e87 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubOrgReaderProcessor.ts @@ -18,6 +18,7 @@ import { LocationSpec } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { GithubCredentialType, + ScmIntegrationRegistry, ScmIntegrations, GithubCredentialsProvider, DefaultGithubCredentialsProvider, @@ -39,7 +40,7 @@ type GraphQL = typeof graphql; * Extracts teams and users out of a GitHub org. */ export class GithubOrgReaderProcessor implements CatalogProcessor { - private readonly integrations: ScmIntegrations; + private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; private readonly githubCredentialsProvider: GithubCredentialsProvider; @@ -59,7 +60,7 @@ export class GithubOrgReaderProcessor implements CatalogProcessor { } constructor(options: { - integrations: ScmIntegrations; + integrations: ScmIntegrationRegistry; logger: Logger; githubCredentialsProvider?: GithubCredentialsProvider; }) { diff --git a/plugins/catalog-backend/src/ingestion/processors/util/parse.ts b/plugins/catalog-backend/src/ingestion/processors/util/parse.ts index c9c6c79311..a6aba7e1f9 100644 --- a/plugins/catalog-backend/src/ingestion/processors/util/parse.ts +++ b/plugins/catalog-backend/src/ingestion/processors/util/parse.ts @@ -17,7 +17,7 @@ import { Entity, LocationSpec, - stringifyLocationReference, + stringifyLocationRef, } from '@backstage/catalog-model'; import lodash from 'lodash'; import yaml from 'yaml'; @@ -32,7 +32,7 @@ export function* parseEntityYaml( try { documents = yaml.parseAllDocuments(data.toString('utf8')).filter(d => d); } catch (e) { - const loc = stringifyLocationReference(location); + const loc = stringifyLocationRef(location); const message = `Failed to parse YAML at ${loc}, ${e}`; yield result.generalError(location, message); return; @@ -40,7 +40,7 @@ export function* parseEntityYaml( for (const document of documents) { if (document.errors?.length) { - const loc = stringifyLocationReference(location); + const loc = stringifyLocationRef(location); const message = `YAML error at ${loc}, ${document.errors[0]}`; yield result.generalError(location, message); } else { diff --git a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts b/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts index b1d44a55b5..20e9966bd1 100644 --- a/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts +++ b/plugins/catalog-backend/src/ingestion/providers/GitHubOrgEntityProvider.ts @@ -14,9 +14,9 @@ * limitations under the License. */ import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, Entity, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { @@ -178,8 +178,8 @@ export function withLocations( { metadata: { annotations: { - [LOCATION_ANNOTATION]: location, - [ORIGIN_LOCATION_ANNOTATION]: location, + [ANNOTATION_LOCATION]: location, + [ANNOTATION_ORIGIN_LOCATION]: location, }, }, }, diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts index 5ae4d58ec7..cc77f6a17b 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.test.ts @@ -16,12 +16,12 @@ import { getVoidLogger } from '@backstage/backend-common'; import { + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, Entity, EntityPolicies, LocationEntity, LocationSpec, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, } from '@backstage/catalog-model'; import { ScmIntegrations } from '@backstage/integration'; import { @@ -82,8 +82,8 @@ describe('DefaultCatalogProcessingOrchestrator', () => { metadata: { name: 'my-foo-bar', annotations: { - [LOCATION_ANNOTATION]: 'url:./here', - [ORIGIN_LOCATION_ANNOTATION]: 'url:./there', + [ANNOTATION_LOCATION]: 'url:./here', + [ANNOTATION_ORIGIN_LOCATION]: 'url:./there', }, }, }; @@ -128,8 +128,8 @@ describe('DefaultCatalogProcessingOrchestrator', () => { metadata: { name: 'my-new-foo-bar', annotations: { - [LOCATION_ANNOTATION]: 'url:./new-place', - [ORIGIN_LOCATION_ANNOTATION]: 'url:./there', + [ANNOTATION_LOCATION]: 'url:./new-place', + [ANNOTATION_ORIGIN_LOCATION]: 'url:./there', }, }, }, @@ -196,8 +196,8 @@ describe('DefaultCatalogProcessingOrchestrator', () => { metadata: { name: 'l', annotations: { - [ORIGIN_LOCATION_ANNOTATION]: 'url:https://example.com/origin.yaml', - [LOCATION_ANNOTATION]: 'url:https://example.com/origin.yaml', + [ANNOTATION_ORIGIN_LOCATION]: 'url:https://example.com/origin.yaml', + [ANNOTATION_LOCATION]: 'url:https://example.com/origin.yaml', }, }, spec: { diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index 756b6a3639..105121ae2a 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -19,9 +19,9 @@ import { EntityPolicy, LocationEntity, LocationSpec, - parseLocationReference, + parseLocationRef, stringifyEntityRef, - stringifyLocationReference, + stringifyLocationRef, } from '@backstage/catalog-model'; import { assertError, @@ -119,10 +119,8 @@ export class DefaultCatalogProcessingOrchestrator // source-location? - maybe probably doesn't exist yet? const context: Context = { entityRef: stringifyEntityRef(entity), - location: parseLocationReference(getEntityLocationRef(entity)), - originLocation: parseLocationReference( - getEntityOriginLocationRef(entity), - ), + location: parseLocationRef(getEntityLocationRef(entity)), + originLocation: parseLocationRef(getEntityOriginLocationRef(entity)), cache, collector, }; @@ -149,9 +147,9 @@ export class DefaultCatalogProcessingOrchestrator throw new NotAllowedError( `Entity ${stringifyEntityRef( deferredEntity.entity, - )} at ${stringifyLocationReference( + )} at ${stringifyLocationRef( context.location, - )}, originated at ${stringifyLocationReference( + )}, originated at ${stringifyLocationRef( context.originLocation, )}, is not of an allowed kind for that location`, ); diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index 4ac356f706..832c5b41bf 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -17,9 +17,9 @@ import { Entity, EntityRelationSpec, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, - stringifyLocationReference, + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, + stringifyLocationRef, } from '@backstage/catalog-model'; import { assertError } from '@backstage/errors'; import { Logger } from 'winston'; @@ -82,7 +82,7 @@ export class ProcessorOutputCollector { return; } - const location = stringifyLocationReference(i.location); + const location = stringifyLocationRef(i.location); // Note that at this point, we have only validated the envelope part of // the entity data. Annotations are not part of that, so we have to be @@ -98,8 +98,8 @@ export class ProcessorOutputCollector { ...entity.metadata, annotations: { ...annotations, - [ORIGIN_LOCATION_ANNOTATION]: originLocation, - [LOCATION_ANNOTATION]: location, + [ANNOTATION_ORIGIN_LOCATION]: originLocation, + [ANNOTATION_LOCATION]: location, }, }, }; diff --git a/plugins/catalog-backend/src/processing/util.ts b/plugins/catalog-backend/src/processing/util.ts index 9f5f23ed09..a4fa984dc1 100644 --- a/plugins/catalog-backend/src/processing/util.ts +++ b/plugins/catalog-backend/src/processing/util.ts @@ -20,8 +20,8 @@ import { entitySchemaValidator, LocationEntity, LocationSpec, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, stringifyEntityRef, } from '@backstage/catalog-model'; import { JsonObject, JsonValue } from '@backstage/types'; @@ -34,7 +34,7 @@ export function isLocationEntity(entity: Entity): entity is LocationEntity { } export function getEntityLocationRef(entity: Entity): string { - const ref = entity.metadata.annotations?.[LOCATION_ANNOTATION]; + const ref = entity.metadata.annotations?.[ANNOTATION_LOCATION]; if (!ref) { const entityRef = stringifyEntityRef(entity); throw new InputError(`Entity '${entityRef}' does not have a location`); @@ -43,7 +43,7 @@ export function getEntityLocationRef(entity: Entity): string { } export function getEntityOriginLocationRef(entity: Entity): string { - const ref = entity.metadata.annotations?.[ORIGIN_LOCATION_ANNOTATION]; + const ref = entity.metadata.annotations?.[ANNOTATION_ORIGIN_LOCATION]; if (!ref) { const entityRef = stringifyEntityRef(entity); throw new InputError( diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts b/plugins/catalog-backend/src/providers/DefaultLocationStore.ts index 9214cd0bc1..f93afedbaa 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts +++ b/plugins/catalog-backend/src/providers/DefaultLocationStore.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Location, LocationSpec } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/catalog-model'; +import { Location } from '@backstage/catalog-client'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts index 93ef6968d1..8e72a265af 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.ts @@ -28,7 +28,7 @@ import { Config } from '@backstage/config'; import { CatalogApi, CatalogClient, - CatalogEntitiesRequest, + GetEntitiesRequest, } from '@backstage/catalog-client'; import { catalogEntityReadPermission } from '@backstage/plugin-catalog-common'; @@ -43,7 +43,7 @@ export interface CatalogEntityDocument extends IndexableDocument { export class DefaultCatalogCollator implements DocumentCollator { protected discovery: PluginEndpointDiscovery; protected locationTemplate: string; - protected filter?: CatalogEntitiesRequest['filter']; + protected filter?: GetEntitiesRequest['filter']; protected readonly catalogClient: CatalogApi; public readonly type: string = 'software-catalog'; public readonly visibilityPermission = catalogEntityReadPermission; @@ -54,7 +54,7 @@ export class DefaultCatalogCollator implements DocumentCollator { options: { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; - filter?: CatalogEntitiesRequest['filter']; + filter?: GetEntitiesRequest['filter']; }, ) { return new DefaultCatalogCollator({ @@ -66,7 +66,7 @@ export class DefaultCatalogCollator implements DocumentCollator { discovery: PluginEndpointDiscovery; tokenManager: TokenManager; locationTemplate?: string; - filter?: CatalogEntitiesRequest['filter']; + filter?: GetEntitiesRequest['filter']; catalogClient?: CatalogApi; }) { const { discovery, locationTemplate, filter, catalogClient, tokenManager } = diff --git a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts index 1ae2833cd0..13557102c9 100644 --- a/plugins/catalog-backend/src/service/AuthorizedLocationService.ts +++ b/plugins/catalog-backend/src/service/AuthorizedLocationService.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { LocationSpec, Location, Entity } from '@backstage/catalog-model'; +import { Location } from '@backstage/catalog-client'; +import { LocationSpec, Entity } from '@backstage/catalog-model'; import { NotAllowedError, NotFoundError } from '@backstage/errors'; import { catalogLocationCreatePermission, diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index 8dd2274842..8bc02e24f6 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -15,12 +15,12 @@ */ import { Entity, - Location, LocationSpec, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, stringifyEntityRef, } from '@backstage/catalog-model'; +import { Location } from '@backstage/catalog-client'; import { CatalogProcessingOrchestrator, DeferredEntity, @@ -112,8 +112,8 @@ export class DefaultLocationService implements LocationService { }), namespace: 'default', annotations: { - [LOCATION_ANNOTATION]: `${spec.type}:${spec.target}`, - [ORIGIN_LOCATION_ANNOTATION]: `${spec.type}:${spec.target}`, + [ANNOTATION_LOCATION]: `${spec.type}:${spec.target}`, + [ANNOTATION_ORIGIN_LOCATION]: `${spec.type}:${spec.target}`, }, }, spec: { diff --git a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts index 1876e59f95..84bfde1259 100644 --- a/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultRefreshService.test.ts @@ -213,6 +213,7 @@ describe('Refresh integration', () => { await engine.stop(); }, + 60_000, ); it.each(databases.eachSupportedId())( @@ -264,6 +265,7 @@ describe('Refresh integration', () => { await engine.stop(); }, + 60_000, ); it.each(databases.eachSupportedId())( @@ -323,5 +325,6 @@ describe('Refresh integration', () => { await engine.stop(); }, + 60_000, ); }); diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts index 205be01b42..6ae24ecd27 100644 --- a/plugins/catalog-backend/src/service/createRouter.test.ts +++ b/plugins/catalog-backend/src/service/createRouter.test.ts @@ -17,7 +17,8 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { NotFoundError } from '@backstage/errors'; -import type { Entity, LocationSpec, Location } from '@backstage/catalog-model'; +import type { Location } from '@backstage/catalog-client'; +import type { Entity, LocationSpec } from '@backstage/catalog-model'; import express from 'express'; import request from 'supertest'; import { EntitiesCatalog } from '../catalog'; diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index ab123238c7..0dce765507 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -15,11 +15,7 @@ */ import { errorHandler } from '@backstage/backend-common'; -import { - analyzeLocationSchema, - locationSpecSchema, - stringifyEntityRef, -} from '@backstage/catalog-model'; +import { stringifyEntityRef } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { NotFoundError } from '@backstage/errors'; import express from 'express'; @@ -34,8 +30,13 @@ import { parseEntityPaginationParams, parseEntityTransformParams, } from './request'; -import { disallowReadonlyMode, validateRequestBody } from './util'; +import { + disallowReadonlyMode, + locationSpec, + validateRequestBody, +} from './util'; import { RefreshOptions, LocationService, RefreshService } from './types'; +import { z } from 'zod'; /** * Options used by {@link createRouter}. @@ -165,7 +166,7 @@ export async function createRouter( if (locationService) { router .post('/locations', async (req, res) => { - const input = await validateRequestBody(req, locationSpecSchema); + const location = await validateRequestBody(req, locationSpec); const dryRun = yn(req.query.dryRun, { default: false }); // when in dryRun addLocation is effectively a read operation so we don't @@ -174,7 +175,7 @@ export async function createRouter( disallowReadonlyMode(readonlyEnabled); } - const output = await locationService.createLocation(input, dryRun, { + const output = await locationService.createLocation(location, dryRun, { authorizationToken: getBearerToken(req.header('authorization')), }); res.status(201).json(output); @@ -206,8 +207,12 @@ export async function createRouter( if (locationAnalyzer) { router.post('/analyze-location', async (req, res) => { - const input = await validateRequestBody(req, analyzeLocationSchema); - const output = await locationAnalyzer.analyzeLocation(input); + const body = await validateRequestBody( + req, + z.object({ location: locationSpec }), + ); + const schema = z.object({ location: locationSpec }); + const output = await locationAnalyzer.analyzeLocation(schema.parse(body)); res.status(200).json(output); }); } diff --git a/plugins/catalog-backend/src/service/types.ts b/plugins/catalog-backend/src/service/types.ts index 02d234b6a8..c923949a04 100644 --- a/plugins/catalog-backend/src/service/types.ts +++ b/plugins/catalog-backend/src/service/types.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Entity, LocationSpec, Location } from '@backstage/catalog-model'; +import { Entity, LocationSpec } from '@backstage/catalog-model'; +import { Location } from '@backstage/catalog-client'; export interface LocationService { createLocation( diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 7cc2d6618a..09f1c2d8cf 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -17,7 +17,7 @@ import { InputError, NotAllowedError } from '@backstage/errors'; import { Request } from 'express'; import lodash from 'lodash'; -import yup from 'yup'; +import { z } from 'zod'; export async function requireRequestBody(req: Request): Promise { const contentType = req.header('content-type'); @@ -40,19 +40,24 @@ export async function requireRequestBody(req: Request): Promise { return body; } +export const locationSpec = z + .object({ + type: z.string(), + target: z.string(), + presence: z.literal('required').or(z.literal('optional')).optional(), + }) + .strict(); // no unknown keys; + export async function validateRequestBody( req: Request, - schema: yup.AnySchema, + schema: z.Schema, ): Promise { const body = await requireRequestBody(req); - try { - await schema.validate(body, { strict: true }); + return await schema.parse(body); } catch (e) { throw new InputError(`Malformed request: ${e}`); } - - return body as unknown as T; } export function disallowReadonlyMode(readonly: boolean) { diff --git a/plugins/catalog-backend/src/util/conversion.ts b/plugins/catalog-backend/src/util/conversion.ts index 559a472c25..9dd5ce7986 100644 --- a/plugins/catalog-backend/src/util/conversion.ts +++ b/plugins/catalog-backend/src/util/conversion.ts @@ -18,10 +18,10 @@ import { Entity, LocationEntityV1alpha1, LocationSpec, - LOCATION_ANNOTATION, - ORIGIN_LOCATION_ANNOTATION, + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, stringifyEntityRef, - stringifyLocationReference, + stringifyLocationRef, } from '@backstage/catalog-model'; import { createHash } from 'crypto'; @@ -40,31 +40,31 @@ export function locationSpecToLocationEntity( let originLocation: string; if (parentEntity) { const maybeOwnLocation = - parentEntity.metadata.annotations?.[LOCATION_ANNOTATION]; + parentEntity.metadata.annotations?.[ANNOTATION_LOCATION]; if (!maybeOwnLocation) { throw new Error( `Parent entity '${stringifyEntityRef( parentEntity, - )}' of location '${stringifyLocationReference( + )}' of location '${stringifyLocationRef( location, )}' does not have a location annotation`, ); } ownLocation = maybeOwnLocation; const maybeOriginLocation = - parentEntity.metadata.annotations?.[ORIGIN_LOCATION_ANNOTATION]; + parentEntity.metadata.annotations?.[ANNOTATION_ORIGIN_LOCATION]; if (!maybeOriginLocation) { throw new Error( `Parent entity '${stringifyEntityRef( parentEntity, - )}' of location '${stringifyLocationReference( + )}' of location '${stringifyLocationRef( location, )}' does not have an origin location annotation`, ); } originLocation = maybeOriginLocation; } else { - ownLocation = stringifyLocationReference(location); + ownLocation = stringifyLocationRef(location); originLocation = ownLocation; } @@ -74,8 +74,8 @@ export function locationSpecToLocationEntity( metadata: { name: locationSpecToMetadataName(location), annotations: { - [LOCATION_ANNOTATION]: ownLocation, - [ORIGIN_LOCATION_ANNOTATION]: originLocation, + [ANNOTATION_LOCATION]: ownLocation, + [ANNOTATION_ORIGIN_LOCATION]: originLocation, }, }, spec: { diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 77bc30964c..a236e55857 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-common +## 0.1.2 + +### Patch Changes + +- ba59832aed: Adds new `catalogEntityCreatePermission` which can be imported and used when authoring a permission policy to restrict/grant a user's access to the catalog import plugin. (And the "Register Existing Component" button which navigates there). + +## 0.1.2-next.0 + +### Patch Changes + +- ba59832aed: Adds new `catalogEntityCreatePermission` which can be imported and used when authoring a permission policy to restrict/grant a user's access to the catalog import plugin. (And the "Register Existing Component" button which navigates there). + ## 0.1.1 ### Patch Changes diff --git a/plugins/catalog-common/api-report.md b/plugins/catalog-common/api-report.md index e2ee2d3225..9826a8ddef 100644 --- a/plugins/catalog-common/api-report.md +++ b/plugins/catalog-common/api-report.md @@ -5,6 +5,9 @@ ```ts import { Permission } from '@backstage/plugin-permission-common'; +// @public +export const catalogEntityCreatePermission: Permission; + // @public export const catalogEntityDeletePermission: Permission; diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 05fc678e59..4b94809ed5 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-common", "description": "Common functionalities for the catalog plugin", - "version": "0.1.1", + "version": "0.1.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,7 +33,7 @@ "@backstage/plugin-permission-common": "^0.4.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0" + "@backstage/cli": "^0.13.2-next.0" }, "files": [ "dist" diff --git a/plugins/catalog-common/src/index.ts b/plugins/catalog-common/src/index.ts index 05d5ed2ba5..9eddf37452 100644 --- a/plugins/catalog-common/src/index.ts +++ b/plugins/catalog-common/src/index.ts @@ -24,6 +24,7 @@ export { RESOURCE_TYPE_CATALOG_ENTITY, catalogEntityReadPermission, + catalogEntityCreatePermission, catalogEntityDeletePermission, catalogEntityRefreshPermission, catalogLocationReadPermission, diff --git a/plugins/catalog-common/src/permissions.ts b/plugins/catalog-common/src/permissions.ts index b4c6c4377c..52c225e441 100644 --- a/plugins/catalog-common/src/permissions.ts +++ b/plugins/catalog-common/src/permissions.ts @@ -38,6 +38,20 @@ export const catalogEntityReadPermission: Permission = { resourceType: RESOURCE_TYPE_CATALOG_ENTITY, }; +/** + * This permission is used to authorize actions that involve creating a new + * catalog entity. This includes registering an existing component into the + * catalog. + * @public + */ +export const catalogEntityCreatePermission: Permission = { + name: 'catalog.entity.create', + attributes: { + action: 'create', + }, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, +}; + /** * This permission is used to designate actions that involve removing one or * more entities from the catalog. diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index b4f2fdac78..0bb6e7224a 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-catalog-graph +## 0.2.10 + +### Patch Changes + +- 7bb1bde7f6: Minor API cleanups +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.2.10-next.0 + +### Patch Changes + +- 7bb1bde7f6: Minor API cleanups +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.2.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.2.8 ### Patch Changes diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index e39982d2b5..515a578d3c 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -95,8 +95,6 @@ export const EntityCatalogGraphCard: ({ // @public export type EntityEdge = DependencyGraphTypes.DependencyEdge; -// Warning: (ae-missing-release-tag) "EntityEdgeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityEdgeData = { relations: string[]; @@ -106,8 +104,6 @@ export type EntityEdgeData = { // @public export type EntityNode = DependencyGraphTypes.DependencyNode; -// Warning: (ae-missing-release-tag) "EntityNodeData" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public export type EntityNodeData = { name: string; diff --git a/plugins/catalog-graph/dev/index.tsx b/plugins/catalog-graph/dev/index.tsx index ed14e5fce9..b9da70c812 100644 --- a/plugins/catalog-graph/dev/index.tsx +++ b/plugins/catalog-graph/dev/index.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { CatalogListResponse } from '@backstage/catalog-client'; +import { GetEntitiesResponse } from '@backstage/catalog-client'; import { Entity, EntityName, @@ -136,7 +136,7 @@ createDevApp() async getEntityByName(name: EntityName): Promise { return entities[stringifyEntityRef(name)]; }, - async getEntities(): Promise> { + async getEntities(): Promise { return { items: Object.values(entities) }; }, } as Partial as unknown as CatalogApi; diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 9b61fd3438..541d21f319 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.8", + "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.5.5", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,10 +42,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts index 830032d527..caf6c8c501 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/types.ts @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DependencyGraphTypes } from '@backstage/core-components'; import { MouseEventHandler } from 'react'; /** - * Additional Data for entities + * Additional Data for entities. + * + * @public */ export type EntityEdgeData = { /** @@ -40,6 +43,8 @@ export type EntityEdge = DependencyGraphTypes.DependencyEdge; /** * Additional data for Entity Node + * + * @public */ export type EntityNodeData = { /** diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 4c10c93354..b757790275 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -39,12 +39,12 @@ "graphql": "^16.0.0", "graphql-tag": "^2.11.0", "graphql-type-json": "^0.3.2", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2-next.0", + "@backstage/test-utils": "^0.2.4-next.0", "@graphql-codegen/cli": "^2.3.1", "@graphql-codegen/typescript": "^2.4.2", "@graphql-codegen/typescript-resolvers": "^2.4.3", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 81fb253736..8c86fd4ca2 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,110 @@ # @backstage/plugin-catalog-import +## 0.8.1 + +### Patch Changes + +- 7bb1bde7f6: Minor API cleanups +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + - @backstage/integration-react@0.1.21 + +## 0.8.1-next.0 + +### Patch Changes + +- 7bb1bde7f6: Minor API cleanups +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + - @backstage/integration-react@0.1.21-next.0 + +## 0.8.0 + +### Minor Changes + +- 2e8764b95f: Make filename, branch name and examples URLs used in catalog import customizable. + + Catalog backend ingestion loop can be already configured to fetch targets with custom catalog filename (other than `catalog-info.yaml`). It's now possible to customize said filename and branch name used in pull requests created by catalog import flow too. This allows organizations to further customize Backstage experience and to better reflect their branding. + + Filename (default: `catalog-info.yaml`) and branch name (default: `backstage-integration`) used in pull requests can be configured in `app-config.yaml` as follows: + + ```yaml + // app-config.yaml + + catalog: + import: + entityFilename: anvil.yaml + pullRequestBranchName: anvil-integration + ``` + + Following React components have also been updated to accept optional props for providing example entity and repository paths: + + ```tsx + + ``` + + ```tsx + + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + - @backstage/integration-react@0.1.20 + +## 0.8.0-next.0 + +### Minor Changes + +- 2e8764b95f: Make filename, branch name and examples URLs used in catalog import customizable. + + Catalog backend ingestion loop can be already configured to fetch targets with custom catalog filename (other than `catalog-info.yaml`). It's now possible to customize said filename and branch name used in pull requests created by catalog import flow too. This allows organizations to further customize Backstage experience and to better reflect their branding. + + Filename (default: `catalog-info.yaml`) and branch name (default: `backstage-integration`) used in pull requests can be configured in `app-config.yaml` as follows: + + ```yaml + // app-config.yaml + + catalog: + import: + entityFilename: anvil.yaml + pullRequestBranchName: anvil-integration + ``` + + Following React components have also been updated to accept optional props for providing example entity and repository paths: + + ```tsx + + ``` + + ```tsx + + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/integration-react@0.1.20-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.7.10 ### Patch Changes diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md index a85777c563..164d831c96 100644 --- a/plugins/catalog-import/api-report.md +++ b/plugins/catalog-import/api-report.md @@ -26,9 +26,7 @@ import { UnpackNestedValue } from 'react-hook-form'; import { UseFormProps } from 'react-hook-form'; import { UseFormReturn } from 'react-hook-form'; -// Warning: (ae-missing-release-tag) "AnalyzeResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type AnalyzeResult = | { type: 'locations'; @@ -45,26 +43,36 @@ export type AnalyzeResult = generatedEntities: PartialEntity[]; }; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "AutocompleteTextField" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const AutocompleteTextField: ({ - name, - options, - required, - errors, - rules, - loading, - loadingText, - helperText, - errorHelperText, - textFieldProps, -}: Props_5) => JSX.Element; +// @public +export const AutocompleteTextField: ( + props: AutocompleteTextFieldProps, +) => JSX.Element; -// Warning: (ae-missing-release-tag) "CatalogImportApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export interface AutocompleteTextFieldProps { + // (undocumented) + errorHelperText?: string; + // (undocumented) + errors?: FieldErrors; + // (undocumented) + helperText?: React_2.ReactNode; + // (undocumented) + loading?: boolean; + // (undocumented) + loadingText?: string; + // (undocumented) + name: TFieldValue; + // (undocumented) + options: string[]; + // (undocumented) + required?: boolean; + // (undocumented) + rules?: React_2.ComponentProps['rules']; + // (undocumented) + textFieldProps?: Omit; +} + +// @public export interface CatalogImportApi { // (undocumented) analyzeUrl(url: string): Promise; @@ -85,14 +93,10 @@ export interface CatalogImportApi { }>; } -// Warning: (ae-missing-release-tag) "catalogImportApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const catalogImportApiRef: ApiRef; -// Warning: (ae-missing-release-tag) "CatalogImportClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export class CatalogImportClient implements CatalogImportApi { constructor(options: { discoveryApi: DiscoveryApi; @@ -110,12 +114,7 @@ export class CatalogImportClient implements CatalogImportApi { body: string; }>; // (undocumented) - submitPullRequest({ - repositoryUrl, - fileContent, - title, - body, - }: { + submitPullRequest(options: { repositoryUrl: string; fileContent: string; title: string; @@ -126,14 +125,10 @@ export class CatalogImportClient implements CatalogImportApi { }>; } -// Warning: (ae-missing-release-tag) "CatalogImportPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const CatalogImportPage: () => JSX.Element; -// Warning: (ae-missing-release-tag) "catalogImportPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public const catalogImportPlugin: BackstagePlugin< { importPage: RouteRef; @@ -143,9 +138,7 @@ const catalogImportPlugin: BackstagePlugin< export { catalogImportPlugin }; export { catalogImportPlugin as plugin }; -// Warning: (ae-forgotten-export) The symbol "ImportFlows" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "StepperProvider" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "defaultGenerateStepper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public export function defaultGenerateStepper( @@ -153,98 +146,221 @@ export function defaultGenerateStepper( defaults: StepperProvider, ): StepperProvider; -// Warning: (ae-missing-release-tag) "DefaultImportPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const DefaultImportPage: () => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityListComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const EntityListComponent: ({ - locations, - collapsed, - locationListItemIcon, - onItemClick, - firstListItem, - withLinks, -}: Props) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ImportInfoCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const ImportInfoCard: ({ - exampleLocationUrl, - exampleRepositoryUrl, -}: Props_2) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "ImportStepper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const ImportStepper: ({ - initialUrl, - generateStepper, - variant, -}: Props_3) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "PreparePullRequestForm" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const PreparePullRequestForm: < +export const EntityListComponent: ( + props: EntityListComponentProps, +) => JSX.Element; + +// @public +export interface EntityListComponentProps { + // (undocumented) + collapsed?: boolean; + // (undocumented) + firstListItem?: React_2.ReactElement; + // (undocumented) + locationListItemIcon: (target: string) => React_2.ReactElement; + // (undocumented) + locations: Array<{ + target: string; + entities: (Entity | EntityName)[]; + }>; + // (undocumented) + onItemClick?: (target: string) => void; + // (undocumented) + withLinks?: boolean; +} + +// @public +export type ImportFlows = + | 'unknown' + | 'single-location' + | 'multiple-locations' + | 'no-location'; + +// @public +export const ImportInfoCard: (props: ImportInfoCardProps) => JSX.Element; + +// @public +export interface ImportInfoCardProps { + // (undocumented) + exampleLocationUrl?: string; + // (undocumented) + exampleRepositoryUrl?: string; +} + +// Warning: (ae-forgotten-export) The symbol "State" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ImportState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ImportState = State & { + activeFlow: ImportFlows; + activeStepNumber: number; + analysisUrl?: string; + onGoBack?: () => void; + onReset: () => void; +}; + +// @public +export const ImportStepper: (props: ImportStepperProps) => JSX.Element; + +// @public +export interface ImportStepperProps { + // (undocumented) + generateStepper?: ( + flow: ImportFlows, + defaults: StepperProvider, + ) => StepperProvider; + // (undocumented) + initialUrl?: string; + // (undocumented) + variant?: InfoCardVariants; +} + +// @public +export const PreparePullRequestForm: >( + props: PreparePullRequestFormProps, +) => JSX.Element; + +// @public +export type PreparePullRequestFormProps< TFieldValues extends Record, ->({ - defaultValues, - onSubmit, - render, -}: Props_6) => JSX.Element; +> = Pick, 'defaultValues'> & { + onSubmit: SubmitHandler; + render: ( + props: Pick< + UseFormReturn, + 'formState' | 'register' | 'control' | 'setValue' + > & { + values: UnpackNestedValue; + }, + ) => React_2.ReactNode; +}; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "PreviewCatalogInfoComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const PreviewCatalogInfoComponent: ({ - repositoryUrl, - entities, - classes, -}: Props_7) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "PreviewPullRequestComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const PreviewPullRequestComponent: ({ - title, - description, - classes, -}: Props_8) => JSX.Element; - -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "StepInitAnalyzeUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public -export const StepInitAnalyzeUrl: ({ - onAnalysis, - analysisUrl, - disablePullRequest, - exampleLocationUrl, -}: Props_4) => JSX.Element; +export type PrepareResult = + | { + type: 'locations'; + locations: Array<{ + exists?: boolean; + target: string; + entities: EntityName[]; + }>; + } + | { + type: 'repository'; + url: string; + integrationType: string; + pullRequest: { + url: string; + }; + locations: Array<{ + target: string; + entities: EntityName[]; + }>; + }; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "StepPrepareCreatePullRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const StepPrepareCreatePullRequest: ({ - analyzeResult, - onPrepare, - onGoBack, - renderFormFields, -}: Props_9) => JSX.Element; +// @public +export const PreviewCatalogInfoComponent: ( + props: PreviewCatalogInfoComponentProps, +) => JSX.Element; + +// @public +export interface PreviewCatalogInfoComponentProps { + // (undocumented) + classes?: { + card?: string; + cardContent?: string; + }; + // (undocumented) + entities: Entity[]; + // (undocumented) + repositoryUrl: string; +} + +// @public +export const PreviewPullRequestComponent: ( + props: PreviewPullRequestComponentProps, +) => JSX.Element; + +// @public +export interface PreviewPullRequestComponentProps { + // (undocumented) + classes?: { + card?: string; + cardContent?: string; + }; + // (undocumented) + description: string; + // (undocumented) + title: string; +} + +// @public +export const StepInitAnalyzeUrl: ( + props: StepInitAnalyzeUrlProps, +) => JSX.Element; + +// @public +export interface StepInitAnalyzeUrlProps { + // (undocumented) + analysisUrl?: string; + // (undocumented) + disablePullRequest?: boolean; + // (undocumented) + exampleLocationUrl?: string; + // (undocumented) + onAnalysis: ( + flow: ImportFlows, + url: string, + result: AnalyzeResult, + opts?: { + prepareResult?: PrepareResult; + }, + ) => void; +} + +// @public +export const StepPrepareCreatePullRequest: ( + props: StepPrepareCreatePullRequestProps, +) => JSX.Element; + +// @public +export interface StepPrepareCreatePullRequestProps { + // (undocumented) + analyzeResult: Extract< + AnalyzeResult, + { + type: 'repository'; + } + >; + // (undocumented) + onGoBack?: () => void; + // (undocumented) + onPrepare: ( + result: PrepareResult, + opts?: { + notRepeatable?: boolean; + }, + ) => void; + // Warning: (ae-forgotten-export) The symbol "FormData" needs to be exported by the entry point index.d.ts + // + // (undocumented) + renderFormFields: ( + props: Pick< + UseFormReturn, + 'register' | 'setValue' | 'formState' + > & { + values: UnpackNestedValue; + groups: string[]; + groupsLoading: boolean; + }, + ) => React_2.ReactNode; +} // Warnings were encountered during analysis: // -// src/api/CatalogImportApi.d.ts:15:5 - (ae-forgotten-export) The symbol "PartialEntity" needs to be exported by the entry point index.d.ts +// src/api/CatalogImportApi.d.ts:25:5 - (ae-forgotten-export) The symbol "PartialEntity" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 2508543541..7ce6f8524f 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.7.10", + "version": "0.8.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,15 +31,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.5.5", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/config": "^0.1.13", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", "@backstage/integration": "^0.7.2", - "@backstage/integration-react": "^0.1.19", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/integration-react": "^0.1.21", + "@backstage/plugin-catalog-react": "^0.6.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -57,16 +57,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 5fae076479..0923f4ac98 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -18,11 +18,20 @@ import { EntityName } from '@backstage/catalog-model'; import { createApiRef } from '@backstage/core-plugin-api'; import { PartialEntity } from '../types'; +/** + * Utility API reference for the {@link CatalogImportApi}. + * + * @public + */ export const catalogImportApiRef = createApiRef({ id: 'plugin.catalog-import.service', }); -// result of the analyze state +/** + * Result of the analysis. + * + * @public + */ export type AnalyzeResult = | { type: 'locations'; @@ -39,6 +48,11 @@ export type AnalyzeResult = generatedEntities: PartialEntity[]; }; +/** + * API for driving catalog imports. + * + * @public + */ export interface CatalogImportApi { analyzeUrl(url: string): Promise; @@ -46,6 +60,7 @@ export interface CatalogImportApi { title: string; body: string; }>; + submitPullRequest(options: { repositoryUrl: string; fileContent: string; diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index 60745923b4..b856a4e359 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -34,6 +34,11 @@ import { getGithubIntegrationConfig } from './GitHub'; import { trimEnd } from 'lodash'; import { getBranchName, getCatalogFilename } from '../components/helpers'; +/** + * The default implementation of the {@link CatalogImportApi}. + * + * @public + */ export class CatalogImportClient implements CatalogImportApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; @@ -142,17 +147,14 @@ the component will become available.\n\nFor more information, read an \ }; } - async submitPullRequest({ - repositoryUrl, - fileContent, - title, - body, - }: { + async submitPullRequest(options: { repositoryUrl: string; fileContent: string; title: string; body: string; }): Promise<{ link: string; location: string }> { + const { repositoryUrl, fileContent, title, body } = options; + const ghConfig = getGithubIntegrationConfig( this.scmIntegrationsApi, repositoryUrl, @@ -172,9 +174,7 @@ the component will become available.\n\nFor more information, read an \ } // TODO: this could be part of the catalog api - private async generateEntityDefinitions({ - repo, - }: { + private async generateEntityDefinitions(options: { repo: string; }): Promise { const { token } = await this.identityApi.getCredentials(); @@ -187,7 +187,7 @@ the component will become available.\n\nFor more information, read an \ }, method: 'POST', body: JSON.stringify({ - location: { type: 'url', target: repo }, + location: { type: 'url', target: options.repo }, }), }, ).catch(e => { @@ -204,12 +204,7 @@ the component will become available.\n\nFor more information, read an \ } // TODO: this response should better be part of the analyze-locations response and scm-independent / implemented per scm - private async checkGitHubForExistingCatalogInfo({ - url, - owner, - repo, - githubIntegrationConfig, - }: { + private async checkGitHubForExistingCatalogInfo(options: { url: string; owner: string; repo: string; @@ -220,6 +215,8 @@ the component will become available.\n\nFor more information, read an \ entities: EntityName[]; }> > { + const { url, owner, repo, githubIntegrationConfig } = options; + const { token } = await this.scmAuthApi.getCredentials({ url }); const octo = new Octokit({ auth: token, @@ -269,15 +266,7 @@ the component will become available.\n\nFor more information, read an \ } // TODO: extract this function and implement for non-github - private async submitGitHubPrToRepo({ - owner, - repo, - title, - body, - fileContent, - repositoryUrl, - githubIntegrationConfig, - }: { + private async submitGitHubPrToRepo(options: { owner: string; repo: string; title: string; @@ -286,6 +275,16 @@ the component will become available.\n\nFor more information, read an \ repositoryUrl: string; githubIntegrationConfig: GitHubIntegrationConfig; }): Promise<{ link: string; location: string }> { + const { + owner, + repo, + title, + body, + fileContent, + repositoryUrl, + githubIntegrationConfig, + } = options; + const { token } = await this.scmAuthApi.getCredentials({ url: repositoryUrl, additionalScope: { diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx index 8693895ec0..30cce9603a 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.tsx @@ -27,6 +27,11 @@ import React from 'react'; import { ImportInfoCard } from '../ImportInfoCard'; import { ImportStepper } from '../ImportStepper'; +/** + * The default catalog import page. + * + * @public + */ export const DefaultImportPage = () => { const configApi = useApi(configApiRef); const appTitle = configApi.getOptional('app.title') || 'Backstage'; diff --git a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx index b8af9942f6..2c59eec18f 100644 --- a/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx +++ b/plugins/catalog-import/src/components/EntityListComponent/EntityListComponent.tsx @@ -47,23 +47,35 @@ function sortEntities(entities: Array) { ); } -type Props = { +/** + * Props for {@link EntityListComponent}. + * + * @public + */ +export interface EntityListComponentProps { locations: Array<{ target: string; entities: (Entity | EntityName)[] }>; locationListItemIcon: (target: string) => React.ReactElement; collapsed?: boolean; firstListItem?: React.ReactElement; onItemClick?: (target: string) => void; withLinks?: boolean; -}; +} + +/** + * Shows a result list of entities. + * + * @public + */ +export const EntityListComponent = (props: EntityListComponentProps) => { + const { + locations, + collapsed = false, + locationListItemIcon, + onItemClick, + firstListItem, + withLinks = false, + } = props; -export const EntityListComponent = ({ - locations, - collapsed = false, - locationListItemIcon, - onItemClick, - firstListItem, - withLinks = false, -}: Props) => { const app = useApp(); const classes = useStyles(); diff --git a/plugins/catalog-import/src/components/EntityListComponent/index.ts b/plugins/catalog-import/src/components/EntityListComponent/index.ts index 06b695240c..7fe0a0d55c 100644 --- a/plugins/catalog-import/src/components/EntityListComponent/index.ts +++ b/plugins/catalog-import/src/components/EntityListComponent/index.ts @@ -15,3 +15,4 @@ */ export { EntityListComponent } from './EntityListComponent'; +export type { EntityListComponentProps } from './EntityListComponent'; diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx index 3e66e137eb..c879cbe998 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.tsx @@ -21,15 +21,27 @@ import React from 'react'; import { catalogImportApiRef } from '../../api'; import { useCatalogFilename } from '../../hooks'; -type Props = { +/** + * Props for {@link ImportInfoCard}. + * + * @public + */ +export interface ImportInfoCardProps { exampleLocationUrl?: string; exampleRepositoryUrl?: string; -}; +} + +/** + * Shows information about the import process. + * + * @public + */ +export const ImportInfoCard = (props: ImportInfoCardProps) => { + const { + exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + exampleRepositoryUrl = 'https://github.com/backstage/backstage', + } = props; -export const ImportInfoCard = ({ - exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', - exampleRepositoryUrl = 'https://github.com/backstage/backstage', -}: Props) => { const configApi = useApi(configApiRef); const appTitle = configApi.getOptional('app.title') || 'Backstage'; const catalogImportApi = useApi(catalogImportApiRef); diff --git a/plugins/catalog-import/src/components/ImportInfoCard/index.ts b/plugins/catalog-import/src/components/ImportInfoCard/index.ts index c82e88f7e8..10dc8726f6 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/index.ts +++ b/plugins/catalog-import/src/components/ImportInfoCard/index.ts @@ -15,3 +15,4 @@ */ export { ImportInfoCard } from './ImportInfoCard'; +export type { ImportInfoCardProps } from './ImportInfoCard'; diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx index 467bd710f0..129e714952 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.tsx @@ -18,6 +18,11 @@ import React from 'react'; import { useOutlet } from 'react-router'; import { DefaultImportPage } from '../DefaultImportPage'; +/** + * The whole catalog import page. + * + * @public + */ export const ImportPage = () => { const outlet = useOutlet(); diff --git a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx index 4112775c2d..b2638e582e 100644 --- a/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx +++ b/plugins/catalog-import/src/components/ImportStepper/ImportStepper.tsx @@ -34,20 +34,32 @@ const useStyles = makeStyles(() => ({ }, })); -type Props = { +/** + * Props for {@link ImportStepper}. + * + * @public + */ +export interface ImportStepperProps { initialUrl?: string; generateStepper?: ( flow: ImportFlows, defaults: StepperProvider, ) => StepperProvider; variant?: InfoCardVariants; -}; +} + +/** + * The stepper that holds the different import stages. + * + * @public + */ +export const ImportStepper = (props: ImportStepperProps) => { + const { + initialUrl, + generateStepper = defaultGenerateStepper, + variant, + } = props; -export const ImportStepper = ({ - initialUrl, - generateStepper = defaultGenerateStepper, - variant, -}: Props) => { const catalogImportApi = useApi(catalogImportApiRef); const classes = useStyles(); const state = useImportState({ initialUrl }); diff --git a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx index 553e8977b8..94dff39d0f 100644 --- a/plugins/catalog-import/src/components/ImportStepper/defaults.tsx +++ b/plugins/catalog-import/src/components/ImportStepper/defaults.tsx @@ -42,7 +42,12 @@ export type StepConfiguration = { content: React.ReactElement; }; -export type StepperProvider = { +/** + * Defines the details of the stepper. + * + * @public + */ +export interface StepperProvider { analyze: ( s: Extract, opts: { apis: StepperApis }, @@ -59,7 +64,7 @@ export type StepperProvider = { s: Extract, opts: { apis: StepperApis }, ) => StepConfiguration; -}; +} /** * The default stepper generation function. @@ -69,6 +74,7 @@ export type StepperProvider = { * * @param flow - the name of the active flow * @param defaults - the default steps + * @public */ export function defaultGenerateStepper( flow: ImportFlows, diff --git a/plugins/catalog-import/src/components/ImportStepper/index.ts b/plugins/catalog-import/src/components/ImportStepper/index.ts index 164c6f43f1..db940f5f74 100644 --- a/plugins/catalog-import/src/components/ImportStepper/index.ts +++ b/plugins/catalog-import/src/components/ImportStepper/index.ts @@ -15,4 +15,5 @@ */ export { ImportStepper } from './ImportStepper'; +export type { ImportStepperProps } from './ImportStepper'; export { defaultGenerateStepper } from './defaults'; diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx index 83377d6b2f..3c655ec11b 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.tsx @@ -27,7 +27,12 @@ type FormData = { url: string; }; -type Props = { +/** + * Props for {@link StepInitAnalyzeUrl}. + * + * @public + */ +export interface StepInitAnalyzeUrlProps { onAnalysis: ( flow: ImportFlows, url: string, @@ -37,7 +42,7 @@ type Props = { disablePullRequest?: boolean; analysisUrl?: string; exampleLocationUrl?: string; -}; +} /** * A form that lets the user input a url and analyze it for existing locations or potential entities. @@ -45,13 +50,16 @@ type Props = { * @param onAnalysis - is called when the analysis was successful * @param analysisUrl - a url that can be used as a default value * @param disablePullRequest - if true, repositories without entities will abort the wizard + * @public */ -export const StepInitAnalyzeUrl = ({ - onAnalysis, - analysisUrl = '', - disablePullRequest = false, - exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', -}: Props) => { +export const StepInitAnalyzeUrl = (props: StepInitAnalyzeUrlProps) => { + const { + onAnalysis, + analysisUrl = '', + disablePullRequest = false, + exampleLocationUrl = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + } = props; + const errorApi = useApi(errorApiRef); const catalogImportApi = useApi(catalogImportApiRef); diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts index 2cd1557d75..2a3dffcf99 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/index.ts @@ -15,3 +15,4 @@ */ export { StepInitAnalyzeUrl } from './StepInitAnalyzeUrl'; +export type { StepInitAnalyzeUrlProps } from './StepInitAnalyzeUrl'; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx index 98d741df15..2e163be2aa 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/AutocompleteTextField.tsx @@ -20,7 +20,12 @@ import { Autocomplete } from '@material-ui/lab'; import React from 'react'; import { Controller, FieldErrors } from 'react-hook-form'; -type Props = { +/** + * Props for {@link AutocompleteTextField}. + * + * @public + */ +export interface AutocompleteTextFieldProps { name: TFieldValue; options: string[]; required?: boolean; @@ -35,20 +40,29 @@ type Props = { errorHelperText?: string; textFieldProps?: Omit; -}; +} + +/** + * An autocompletion text field for the catalog import flows. + * + * @public + */ +export const AutocompleteTextField = ( + props: AutocompleteTextFieldProps, +) => { + const { + name, + options, + required, + errors, + rules, + loading = false, + loadingText, + helperText, + errorHelperText, + textFieldProps = {}, + } = props; -export const AutocompleteTextField = ({ - name, - options, - required, - errors, - rules, - loading = false, - loadingText, - helperText, - errorHelperText, - textFieldProps = {}, -}: Props) => { return ( > = Pick< - UseFormProps, - 'defaultValues' -> & { +/** + * Props for {@link PreparePullRequestForm}. + * + * @public + */ +export type PreparePullRequestFormProps< + TFieldValues extends Record, +> = Pick, 'defaultValues'> & { onSubmit: SubmitHandler; render: ( @@ -48,14 +52,15 @@ type Props> = Pick< * @param onSubmit - a callback that is executed when the form is submitted * (initiated by a button of type="submit") * @param render - render the form elements + * @public */ export const PreparePullRequestForm = < TFieldValues extends Record, ->({ - defaultValues, - onSubmit, - render, -}: Props) => { +>( + props: PreparePullRequestFormProps, +) => { + const { defaultValues, onSubmit, render } = props; + const methods = useForm({ mode: 'onTouched', defaultValues }); const { handleSubmit, watch, control, register, formState, setValue } = methods; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx index 0f1dc33d2e..a16276f90b 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.tsx @@ -22,17 +22,26 @@ import { CodeSnippet } from '@backstage/core-components'; import { trimEnd } from 'lodash'; import { useCatalogFilename } from '../../hooks'; -type Props = { +/** + * Props for {@link PreviewCatalogInfoComponent}. + * + * @public + */ +export interface PreviewCatalogInfoComponentProps { repositoryUrl: string; entities: Entity[]; classes?: { card?: string; cardContent?: string }; -}; +} -export const PreviewCatalogInfoComponent = ({ - repositoryUrl, - entities, - classes, -}: Props) => { +/** + * Previews information about an entity to create. + * + * @public + */ +export const PreviewCatalogInfoComponent = ( + props: PreviewCatalogInfoComponentProps, +) => { + const { repositoryUrl, entities, classes } = props; const catalogFilename = useCatalogFilename(); return ( diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx index 7b6653e4ca..e8a54489a0 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewPullRequestComponent.tsx @@ -18,17 +18,26 @@ import { Card, CardContent, CardHeader } from '@material-ui/core'; import React from 'react'; import { MarkdownContent } from '@backstage/core-components'; -type Props = { +/** + * Props for {@link PreviewPullRequestComponent}. + * + * @public + */ +export interface PreviewPullRequestComponentProps { title: string; description: string; classes?: { card?: string; cardContent?: string }; -}; +} -export const PreviewPullRequestComponent = ({ - title, - description, - classes, -}: Props) => { +/** + * Previews a pull request. + * + * @public + */ +export const PreviewPullRequestComponent = ( + props: PreviewPullRequestComponentProps, +) => { + const { title, description, classes } = props; return ( diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx index a79caaa0b9..36e8c33a5c 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx @@ -53,7 +53,12 @@ type FormData = { useCodeowners: boolean; }; -type Props = { +/** + * Props for {@link StepPrepareCreatePullRequest}. + * + * @public + */ +export interface StepPrepareCreatePullRequestProps { analyzeResult: Extract; onPrepare: ( result: PrepareResult, @@ -71,7 +76,7 @@ type Props = { groupsLoading: boolean; }, ) => React.ReactNode; -}; +} export function generateEntities( entities: PartialEntity[], @@ -93,12 +98,16 @@ export function generateEntities( })); } -export const StepPrepareCreatePullRequest = ({ - analyzeResult, - onPrepare, - onGoBack, - renderFormFields, -}: Props) => { +/** + * Prepares a pull request. + * + * @public + */ +export const StepPrepareCreatePullRequest = ( + props: StepPrepareCreatePullRequestProps, +) => { + const { analyzeResult, onPrepare, onGoBack, renderFormFields } = props; + const classes = useStyles(); const catalogApi = useApi(catalogApiRef); const catalogImportApi = useApi(catalogImportApiRef); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts index 1e8eaea1b9..ce1cd40e19 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/index.ts @@ -15,7 +15,12 @@ */ export { AutocompleteTextField } from './AutocompleteTextField'; +export type { AutocompleteTextFieldProps } from './AutocompleteTextField'; export { PreparePullRequestForm } from './PreparePullRequestForm'; +export type { PreparePullRequestFormProps } from './PreparePullRequestForm'; export { PreviewCatalogInfoComponent } from './PreviewCatalogInfoComponent'; +export type { PreviewCatalogInfoComponentProps } from './PreviewCatalogInfoComponent'; export { PreviewPullRequestComponent } from './PreviewPullRequestComponent'; +export type { PreviewPullRequestComponentProps } from './PreviewPullRequestComponent'; export { StepPrepareCreatePullRequest } from './StepPrepareCreatePullRequest'; +export type { StepPrepareCreatePullRequestProps } from './StepPrepareCreatePullRequest'; diff --git a/plugins/catalog-import/src/components/index.ts b/plugins/catalog-import/src/components/index.ts index 886c679d2d..15f57e7219 100644 --- a/plugins/catalog-import/src/components/index.ts +++ b/plugins/catalog-import/src/components/index.ts @@ -20,3 +20,4 @@ export * from './ImportInfoCard'; export * from './ImportStepper'; export * from './StepInitAnalyzeUrl'; export * from './StepPrepareCreatePullRequest'; +export type { ImportFlows, ImportState, PrepareResult } from './useImportState'; diff --git a/plugins/catalog-import/src/components/useImportState.ts b/plugins/catalog-import/src/components/useImportState.ts index a1505b26b5..0cb6b59917 100644 --- a/plugins/catalog-import/src/components/useImportState.ts +++ b/plugins/catalog-import/src/components/useImportState.ts @@ -18,7 +18,11 @@ import { Entity, EntityName } from '@backstage/catalog-model'; import { useReducer } from 'react'; import { AnalyzeResult } from '../api'; -// the configuration of the stepper +/** + * The configuration of the stepper. + * + * @public + */ export type ImportFlows = | 'unknown' | 'single-location' @@ -28,7 +32,11 @@ export type ImportFlows = // the available states of the stepper type ImportStateTypes = 'analyze' | 'prepare' | 'review' | 'finish'; -// result of the prepare state +/** + * Result of the prepare state. + * + * @public + */ export type PrepareResult = | { type: 'locations'; diff --git a/plugins/catalog-import/src/plugin.ts b/plugins/catalog-import/src/plugin.ts index 52c1929dce..d9dd226792 100644 --- a/plugins/catalog-import/src/plugin.ts +++ b/plugins/catalog-import/src/plugin.ts @@ -34,6 +34,12 @@ export const rootRouteRef = createRouteRef({ id: 'catalog-import', }); +/** + * A plugin that helps the user in importing projects and YAML files into the + * catalog. + * + * @public + */ export const catalogImportPlugin = createPlugin({ id: 'catalog-import', apis: [ @@ -70,6 +76,11 @@ export const catalogImportPlugin = createPlugin({ }, }); +/** + * The page for importing projects and YAML files into the catalog. + * + * @public + */ export const CatalogImportPage = catalogImportPlugin.provide( createRoutableExtension({ name: 'CatalogImportPage', diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index a98178af1d..65fd275389 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/plugin-catalog-react +## 0.6.14 + +### Patch Changes + +- 680e7c7452: Updated `useEntityListProvider` and catalog pickers to respond to external changes to query parameters in the URL, such as two sidebar links that apply different catalog filters. +- f8633307c4: Added an "inspect" entry in the entity three-dots menu, for lower level catalog + insights and debugging. +- 19155e0939: Updated React component type declarations to avoid exporting exotic component types. +- 7bb1bde7f6: Minor API cleanups +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/core-components@0.8.8 + +## 0.6.14-next.0 + +### Patch Changes + +- 680e7c7452: Updated `useEntityListProvider` and catalog pickers to respond to external changes to query parameters in the URL, such as two sidebar links that apply different catalog filters. +- 7bb1bde7f6: Minor API cleanups +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.6.13 + +### Patch Changes + +- f7257dff6f: The `` component now accepts a `noTrack` prop, which prevents the `click` event from being captured by the Analytics API. This can be used if tracking is explicitly not warranted, or in order to use custom link tracking in specific situations. +- 300f8cdaee: Fix bug: previously the filter would be set to "all" on page load, even if the + `initiallySelectedFilter` on the `DefaultCatalogPage` was set to something else, + or a different query parameter was supplied. Now, the prop and query parameters + control the filter as expected. Additionally, after this change any filters + which match 0 items will be disabled, and the filter will be reverted to 'all' + if they're set on page load. +- 6acc8f7db7: Add caching to the useEntityPermission hook + + The hook now caches the authorization decision based on the permission + the entity, and returns the cache match value as the default `allowed` value while loading. This helps avoid flicker in UI elements that would be conditionally rendered based on the `allowed` result of this hook. + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.6.13-next.1 + +### Patch Changes + +- f7257dff6f: The `` component now accepts a `noTrack` prop, which prevents the `click` event from being captured by the Analytics API. This can be used if tracking is explicitly not warranted, or in order to use custom link tracking in specific situations. +- 300f8cdaee: Fix bug: previously the filter would be set to "all" on page load, even if the + `initiallySelectedFilter` on the `DefaultCatalogPage` was set to something else, + or a different query parameter was supplied. Now, the prop and query parameters + control the filter as expected. Additionally, after this change any filters + which match 0 items will be disabled, and the filter will be reverted to 'all' + if they're set on page load. +- 6acc8f7db7: Add caching to the useEntityPermission hook + + The hook now caches the authorization decision based on the permission + the entity, and returns the cache match value as the default `allowed` value while loading. This helps avoid flicker in UI elements that would be conditionally rendered based on the `allowed` result of this hook. + +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + +## 0.6.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.6.12 ### Patch Changes diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 398bace903..5ab3f20d76 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -9,12 +9,12 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; -import { CatalogListResponse } from '@backstage/catalog-client'; import { ComponentEntity } from '@backstage/catalog-model'; import { ComponentProps } from 'react'; import { Context } from 'react'; import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; +import { GetEntitiesResponse } from '@backstage/catalog-client'; import { IconButton } from '@material-ui/core'; import { IdentityApi } from '@backstage/core-plugin-api'; import { LinkProps } from '@backstage/core-components'; @@ -105,22 +105,14 @@ export type CatalogReactUserListPickerClassKey = // @public @deprecated (undocumented) export const catalogRouteRef: RouteRef; -// Warning: (ae-missing-release-tag) "createDomainColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createDomainColumn(): TableColumn; -// Warning: (ae-missing-release-tag) "createEntityRefColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -function createEntityRefColumn({ - defaultKind, -}: { +function createEntityRefColumn(options: { defaultKind?: string; }): TableColumn; -// Warning: (ae-missing-release-tag) "createEntityRelationColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createEntityRelationColumn({ title, @@ -136,28 +128,18 @@ function createEntityRelationColumn({ }; }): TableColumn; -// Warning: (ae-missing-release-tag) "createMetadataDescriptionColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createMetadataDescriptionColumn(): TableColumn; -// Warning: (ae-missing-release-tag) "createOwnerColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createOwnerColumn(): TableColumn; -// Warning: (ae-missing-release-tag) "createSpecLifecycleColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createSpecLifecycleColumn(): TableColumn; -// Warning: (ae-missing-release-tag) "createSpecTypeColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createSpecTypeColumn(): TableColumn; -// Warning: (ae-missing-release-tag) "createSystemColumn" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) function createSystemColumn(): TableColumn; @@ -215,14 +197,18 @@ export class EntityKindFilter implements EntityFilter { readonly value: string; } -// Warning: (ae-forgotten-export) The symbol "EntityKindFilterProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityKindPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) -export const EntityKindPicker: ({ - initialFilter, - hidden, -}: EntityKindFilterProps) => JSX.Element | null; +export const EntityKindPicker: ( + props: EntityKindPickerProps, +) => JSX.Element | null; + +// @public +export interface EntityKindPickerProps { + // (undocumented) + hidden: boolean; + // (undocumented) + initialFilter?: string; +} // Warning: (ae-missing-release-tag) "EntityLifecycleFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -237,8 +223,6 @@ export class EntityLifecycleFilter implements EntityFilter { readonly values: string[]; } -// Warning: (ae-missing-release-tag) "EntityLifecyclePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntityLifecyclePicker: () => JSX.Element | null; @@ -270,8 +254,6 @@ export class EntityOwnerFilter implements EntityFilter { readonly values: string[]; } -// Warning: (ae-missing-release-tag) "EntityOwnerPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntityOwnerPicker: () => JSX.Element | null; @@ -289,307 +271,30 @@ export interface EntityProviderProps { entity?: Entity; } -// Warning: (ae-forgotten-export) The symbol "EntityRefLinkProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityRefLink" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const EntityRefLink: React_2.ForwardRefExoticComponent< - Pick< - EntityRefLinkProps, - | 'replace' - | 'type' - | 'key' - | 'id' - | 'media' - | 'state' - | 'color' - | 'display' - | 'translate' - | 'hidden' - | 'dir' - | 'slot' - | 'style' - | 'title' - | 'target' - | 'accessKey' - | 'draggable' - | 'lang' - | 'className' - | 'prefix' - | 'children' - | 'contentEditable' - | 'inputMode' - | 'tabIndex' - | 'underline' - | 'download' - | 'href' - | 'hrefLang' - | 'defaultChecked' - | 'defaultValue' - | 'suppressContentEditableWarning' - | 'suppressHydrationWarning' - | 'contextMenu' - | 'placeholder' - | 'spellCheck' - | 'radioGroup' - | 'role' - | 'about' - | 'datatype' - | 'inlist' - | 'property' - | 'resource' - | 'typeof' - | 'vocab' - | 'autoCapitalize' - | 'autoCorrect' - | 'autoSave' - | 'itemProp' - | 'itemScope' - | 'itemType' - | 'itemID' - | 'itemRef' - | 'results' - | 'security' - | 'unselectable' - | 'is' - | 'aria-activedescendant' - | 'aria-atomic' - | 'aria-autocomplete' - | 'aria-busy' - | 'aria-checked' - | 'aria-colcount' - | 'aria-colindex' - | 'aria-colspan' - | 'aria-controls' - | 'aria-current' - | 'aria-describedby' - | 'aria-details' - | 'aria-disabled' - | 'aria-dropeffect' - | 'aria-errormessage' - | 'aria-expanded' - | 'aria-flowto' - | 'aria-grabbed' - | 'aria-haspopup' - | 'aria-hidden' - | 'aria-invalid' - | 'aria-keyshortcuts' - | 'aria-label' - | 'aria-labelledby' - | 'aria-level' - | 'aria-live' - | 'aria-modal' - | 'aria-multiline' - | 'aria-multiselectable' - | 'aria-orientation' - | 'aria-owns' - | 'aria-placeholder' - | 'aria-posinset' - | 'aria-pressed' - | 'aria-readonly' - | 'aria-relevant' - | 'aria-required' - | 'aria-roledescription' - | 'aria-rowcount' - | 'aria-rowindex' - | 'aria-rowspan' - | 'aria-selected' - | 'aria-setsize' - | 'aria-sort' - | 'aria-valuemax' - | 'aria-valuemin' - | 'aria-valuenow' - | 'aria-valuetext' - | 'rel' - | 'dangerouslySetInnerHTML' - | 'onCopy' - | 'onCopyCapture' - | 'onCut' - | 'onCutCapture' - | 'onPaste' - | 'onPasteCapture' - | 'onCompositionEnd' - | 'onCompositionEndCapture' - | 'onCompositionStart' - | 'onCompositionStartCapture' - | 'onCompositionUpdate' - | 'onCompositionUpdateCapture' - | 'onFocus' - | 'onFocusCapture' - | 'onBlur' - | 'onBlurCapture' - | 'onChange' - | 'onChangeCapture' - | 'onBeforeInput' - | 'onBeforeInputCapture' - | 'onInput' - | 'onInputCapture' - | 'onReset' - | 'onResetCapture' - | 'onSubmit' - | 'onSubmitCapture' - | 'onInvalid' - | 'onInvalidCapture' - | 'onLoad' - | 'onLoadCapture' - | 'onError' - | 'onErrorCapture' - | 'onKeyDown' - | 'onKeyDownCapture' - | 'onKeyPress' - | 'onKeyPressCapture' - | 'onKeyUp' - | 'onKeyUpCapture' - | 'onAbort' - | 'onAbortCapture' - | 'onCanPlay' - | 'onCanPlayCapture' - | 'onCanPlayThrough' - | 'onCanPlayThroughCapture' - | 'onDurationChange' - | 'onDurationChangeCapture' - | 'onEmptied' - | 'onEmptiedCapture' - | 'onEncrypted' - | 'onEncryptedCapture' - | 'onEnded' - | 'onEndedCapture' - | 'onLoadedData' - | 'onLoadedDataCapture' - | 'onLoadedMetadata' - | 'onLoadedMetadataCapture' - | 'onLoadStart' - | 'onLoadStartCapture' - | 'onPause' - | 'onPauseCapture' - | 'onPlay' - | 'onPlayCapture' - | 'onPlaying' - | 'onPlayingCapture' - | 'onProgress' - | 'onProgressCapture' - | 'onRateChange' - | 'onRateChangeCapture' - | 'onSeeked' - | 'onSeekedCapture' - | 'onSeeking' - | 'onSeekingCapture' - | 'onStalled' - | 'onStalledCapture' - | 'onSuspend' - | 'onSuspendCapture' - | 'onTimeUpdate' - | 'onTimeUpdateCapture' - | 'onVolumeChange' - | 'onVolumeChangeCapture' - | 'onWaiting' - | 'onWaitingCapture' - | 'onAuxClick' - | 'onAuxClickCapture' - | 'onClick' - | 'onClickCapture' - | 'onContextMenu' - | 'onContextMenuCapture' - | 'onDoubleClick' - | 'onDoubleClickCapture' - | 'onDrag' - | 'onDragCapture' - | 'onDragEnd' - | 'onDragEndCapture' - | 'onDragEnter' - | 'onDragEnterCapture' - | 'onDragExit' - | 'onDragExitCapture' - | 'onDragLeave' - | 'onDragLeaveCapture' - | 'onDragOver' - | 'onDragOverCapture' - | 'onDragStart' - | 'onDragStartCapture' - | 'onDrop' - | 'onDropCapture' - | 'onMouseDown' - | 'onMouseDownCapture' - | 'onMouseEnter' - | 'onMouseLeave' - | 'onMouseMove' - | 'onMouseMoveCapture' - | 'onMouseOut' - | 'onMouseOutCapture' - | 'onMouseOver' - | 'onMouseOverCapture' - | 'onMouseUp' - | 'onMouseUpCapture' - | 'onSelect' - | 'onSelectCapture' - | 'onTouchCancel' - | 'onTouchCancelCapture' - | 'onTouchEnd' - | 'onTouchEndCapture' - | 'onTouchMove' - | 'onTouchMoveCapture' - | 'onTouchStart' - | 'onTouchStartCapture' - | 'onPointerDown' - | 'onPointerDownCapture' - | 'onPointerMove' - | 'onPointerMoveCapture' - | 'onPointerUp' - | 'onPointerUpCapture' - | 'onPointerCancel' - | 'onPointerCancelCapture' - | 'onPointerEnter' - | 'onPointerEnterCapture' - | 'onPointerLeave' - | 'onPointerLeaveCapture' - | 'onPointerOver' - | 'onPointerOverCapture' - | 'onPointerOut' - | 'onPointerOutCapture' - | 'onGotPointerCapture' - | 'onGotPointerCaptureCapture' - | 'onLostPointerCapture' - | 'onLostPointerCaptureCapture' - | 'onScroll' - | 'onScrollCapture' - | 'onWheel' - | 'onWheelCapture' - | 'onAnimationStart' - | 'onAnimationStartCapture' - | 'onAnimationEnd' - | 'onAnimationEndCapture' - | 'onAnimationIteration' - | 'onAnimationIterationCapture' - | 'onTransitionEnd' - | 'onTransitionEndCapture' - | 'ping' - | 'referrerPolicy' - | 'align' - | 'variant' - | 'component' - | 'classes' - | 'innerRef' - | 'noWrap' - | 'gutterBottom' - | 'paragraph' - | 'variantMapping' - | 'TypographyClasses' - | 'entityRef' - | 'defaultKind' - > & - React_2.RefAttributes ->; +// @public +export const EntityRefLink: (props: EntityRefLinkProps) => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "EntityRefLinksProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityRefLinks" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type EntityRefLinkProps = { + entityRef: Entity | EntityName; + defaultKind?: string; + title?: string; + children?: React_2.ReactNode; +} & Omit; + +// @public export const EntityRefLinks: ({ entityRefs, defaultKind, ...linkProps }: EntityRefLinksProps) => JSX.Element; +// @public +export type EntityRefLinksProps = { + entityRefs: (Entity | EntityName)[]; + defaultKind?: string; +} & Omit; + // Warning: (ae-missing-release-tag) "entityRoute" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated (undocumented) @@ -617,8 +322,6 @@ export const entityRouteRef: RouteRef<{ namespace: string; }>; -// Warning: (ae-missing-release-tag) "EntitySearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntitySearchBar: () => JSX.Element; @@ -630,18 +333,12 @@ export type EntitySourceLocation = { integrationType?: string; }; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntityTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-missing-release-tag) "EntityTable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) -export function EntityTable({ - entities, - title, - emptyContent, - variant, - columns, -}: Props): JSX.Element; +// @public +export function EntityTable( + props: EntityTableProps, +): JSX.Element; // @public (undocumented) export namespace EntityTable { @@ -655,6 +352,22 @@ export namespace EntityTable { componentEntityColumns: TableColumn[]; } +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "EntityTable" has more than one declaration; you need to add a TSDoc member reference selector +// +// @public +export interface EntityTableProps { + // (undocumented) + columns: TableColumn[]; + // (undocumented) + emptyContent?: ReactNode; + // (undocumented) + entities: T[]; + // (undocumented) + title: string; + // (undocumented) + variant?: 'gridItem'; +} + // Warning: (ae-missing-release-tag) "EntityTagFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -668,8 +381,6 @@ export class EntityTagFilter implements EntityFilter { readonly values: string[]; } -// Warning: (ae-missing-release-tag) "EntityTagPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntityTagPicker: () => JSX.Element | null; @@ -699,26 +410,24 @@ export class EntityTypeFilter implements EntityFilter { readonly value: string | string[]; } -// Warning: (ae-missing-release-tag) "EntityTypeFilterProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type EntityTypeFilterProps = { - initialFilter?: string; - hidden?: boolean; -}; - -// Warning: (ae-missing-release-tag) "EntityTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export const EntityTypePicker: ( - props: EntityTypeFilterProps, + props: EntityTypePickerProps, ) => JSX.Element | null; +// @public +export interface EntityTypePickerProps { + // (undocumented) + hidden?: boolean; + // (undocumented) + initialFilter?: string; +} + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "FavoriteEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export const FavoriteEntity: (props: Props_2) => JSX.Element; +export const FavoriteEntity: (props: Props) => JSX.Element; // Warning: (ae-missing-release-tag) "favoriteEntityIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -771,6 +480,13 @@ export function getEntitySourceLocation( scmIntegrationsApi: ScmIntegrationRegistry, ): EntitySourceLocation | undefined; +// @public +export function InspectEntityDialog(props: { + open: boolean; + entity: Entity; + onClose: () => void; +}): JSX.Element | null; + // Warning: (ae-missing-release-tag) "isOwnerOf" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -834,7 +550,7 @@ export const UnregisterEntityDialog: ({ onConfirm, onClose, entity, -}: Props_3) => JSX.Element; +}: Props_2) => JSX.Element; // @public export function useEntity(): { @@ -896,7 +612,7 @@ export function useEntityTypeFilter(): EntityTypeReturn; // @public export function useOwnedEntities(allowedKinds?: string[]): { loading: boolean; - ownedEntities: CatalogListResponse | undefined; + ownedEntities: GetEntitiesResponse | undefined; }; // Warning: (ae-missing-release-tag) "useOwnUser" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 66045c1ff1..d995fdf848 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "0.6.12", + "version": "0.6.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.5.5", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", "@backstage/integration": "^0.7.2", @@ -42,11 +42,13 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", + "classnames": "^2.2.6", "jwt-decode": "^3.1.0", "lodash": "^4.17.21", "qs": "^6.9.4", "react-router": "6.0.0-beta.0", "react-use": "^17.2.4", + "yaml": "^1.10.0", "zen-observable": "^0.8.15" }, "peerDependencies": { @@ -54,10 +56,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/plugin-catalog-common": "^0.1.1", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/plugin-catalog-common": "^0.1.2", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -65,7 +67,7 @@ "@types/jest": "^26.0.7", "@types/jwt-decode": "^3.1.0", "@types/zen-observable": "^0.8.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "react-test-renderer": "^16.13.1" }, "files": [ diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx index 6f9119c404..17d28aad9f 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.tsx @@ -19,15 +19,20 @@ import { Alert } from '@material-ui/lab'; import { useEntityListProvider } from '../../hooks'; import { EntityKindFilter } from '../../filters'; -type EntityKindFilterProps = { +/** + * Props for {@link EntityKindPicker}. + * + * @public + */ +export interface EntityKindPickerProps { initialFilter?: string; hidden: boolean; -}; +} + +/** @public */ +export const EntityKindPicker = (props: EntityKindPickerProps) => { + const { initialFilter, hidden } = props; -export const EntityKindPicker = ({ - initialFilter, - hidden, -}: EntityKindFilterProps) => { const { updateFilters, queryParameters } = useEntityListProvider(); const [selectedKind] = useState( [queryParameters.kind].flat()[0] ?? initialFilter, diff --git a/plugins/catalog-react/src/components/EntityKindPicker/index.ts b/plugins/catalog-react/src/components/EntityKindPicker/index.ts index 89dd46230b..a89a96aebc 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/index.ts +++ b/plugins/catalog-react/src/components/EntityKindPicker/index.ts @@ -15,3 +15,4 @@ */ export { EntityKindPicker } from './EntityKindPicker'; +export type { EntityKindPickerProps } from './EntityKindPicker'; diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx index f7d915ef5b..47e85292e2 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx @@ -161,4 +161,34 @@ describe('', () => { lifecycles: undefined, }); }); + + it('responds to external queryParameters changes', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + lifecycles: new EntityLifecycleFilter(['experimental']), + }); + rendered.rerender( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + lifecycles: new EntityLifecycleFilter(['production']), + }); + }); }); diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx index 630b1700a0..77691ab7c5 100644 --- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx +++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx @@ -46,20 +46,31 @@ const useStyles = makeStyles( const icon = ; const checkedIcon = ; +/** @public */ export const EntityLifecyclePicker = () => { const classes = useStyles(); const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); - const queryParamLifecycles = [queryParameters.lifecycles] - .flat() - .filter(Boolean) as string[]; + const queryParamLifecycles = useMemo( + () => [queryParameters.lifecycles].flat().filter(Boolean) as string[], + [queryParameters], + ); + const [selectedLifecycles, setSelectedLifecycles] = useState( queryParamLifecycles.length ? queryParamLifecycles : filters.lifecycles?.values ?? [], ); + // Set selected lifecycles on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + if (queryParamLifecycles.length) { + setSelectedLifecycles(queryParamLifecycles); + } + }, [queryParamLifecycles]); + useEffect(() => { updateFilters({ lifecycles: selectedLifecycles.length diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx index 277a691a84..14abc358db 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx @@ -191,4 +191,34 @@ describe('', () => { owner: undefined, }); }); + + it('responds to external queryParameters changes', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + owners: new EntityOwnerFilter(['team-a']), + }); + rendered.rerender( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + owners: new EntityOwnerFilter(['team-b']), + }); + }); }); diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index ae88858666..acb5ee9279 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -48,18 +48,29 @@ const useStyles = makeStyles( const icon = ; const checkedIcon = ; +/** @public */ export const EntityOwnerPicker = () => { const classes = useStyles(); const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); - const queryParamOwners = [queryParameters.owners] - .flat() - .filter(Boolean) as string[]; + const queryParamOwners = useMemo( + () => [queryParameters.owners].flat().filter(Boolean) as string[], + [queryParameters], + ); + const [selectedOwners, setSelectedOwners] = useState( queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [], ); + // Set selected owners on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + if (queryParamOwners.length) { + setSelectedOwners(queryParamOwners); + } + }, [queryParamOwners]); + useEffect(() => { updateFilters({ owners: selectedOwners.length diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx index 02e63e60e3..8120ececd2 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLink.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity, EntityName, @@ -25,6 +26,11 @@ import { Link, LinkProps } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; import { Tooltip } from '@material-ui/core'; +/** + * Props for {@link EntityRefLink}. + * + * @public + */ export type EntityRefLinkProps = { entityRef: Entity | EntityName; defaultKind?: string; @@ -32,6 +38,11 @@ export type EntityRefLinkProps = { children?: React.ReactNode; } & Omit; +/** + * Shows a clickable link to an entity. + * + * @public + */ export const EntityRefLink = forwardRef( (props, ref) => { const { entityRef, defaultKind, title, children, ...linkProps } = props; @@ -76,4 +87,4 @@ export const EntityRefLink = forwardRef( link ); }, -); +) as (props: EntityRefLinkProps) => JSX.Element; diff --git a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx index 4a990f4028..9be970aa56 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx +++ b/plugins/catalog-react/src/components/EntityRefLink/EntityRefLinks.tsx @@ -13,16 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Entity, EntityName } from '@backstage/catalog-model'; import React from 'react'; import { EntityRefLink } from './EntityRefLink'; import { LinkProps } from '@backstage/core-components'; +/** + * Props for {@link EntityRefLink}. + * + * @public + */ export type EntityRefLinksProps = { entityRefs: (Entity | EntityName)[]; defaultKind?: string; } & Omit; +/** + * Shows a list of clickable links to entities. + * + * @public + */ export const EntityRefLinks = ({ entityRefs, defaultKind, diff --git a/plugins/catalog-react/src/components/EntityRefLink/format.ts b/plugins/catalog-react/src/components/EntityRefLink/format.ts index f3977ccc95..4787474c68 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/format.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/format.ts @@ -18,7 +18,6 @@ import { Entity, EntityName, ENTITY_DEFAULT_NAMESPACE, - serializeEntityRef, } from '@backstage/catalog-model'; export function formatEntityRefTitle( @@ -45,13 +44,9 @@ export function formatEntityRefTitle( } kind = kind.toLocaleLowerCase('en-US'); - - return `${serializeEntityRef({ - kind: - defaultKind && defaultKind.toLocaleLowerCase('en-US') === kind - ? undefined - : kind, - name, - namespace, - })}`; + kind = + defaultKind && defaultKind.toLocaleLowerCase('en-US') === kind + ? undefined + : kind; + return `${kind ? `${kind}:` : ''}${namespace ? `${namespace}/` : ''}${name}`; } diff --git a/plugins/catalog-react/src/components/EntityRefLink/index.ts b/plugins/catalog-react/src/components/EntityRefLink/index.ts index 1c5297e2f2..d49993feb6 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/index.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/index.ts @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { EntityRefLink } from './EntityRefLink'; +export type { EntityRefLinkProps } from './EntityRefLink'; export { EntityRefLinks } from './EntityRefLinks'; +export type { EntityRefLinksProps } from './EntityRefLinks'; export { formatEntityRefTitle } from './format'; diff --git a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx index 79b2ee36bf..ec977fada4 100644 --- a/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx +++ b/plugins/catalog-react/src/components/EntitySearchBar/EntitySearchBar.tsx @@ -45,6 +45,7 @@ const useStyles = makeStyles( }, ); +/** @public */ export const EntitySearchBar = () => { const classes = useStyles(); diff --git a/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx b/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx index 43da2b3612..62c33701b4 100644 --- a/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx +++ b/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx @@ -21,13 +21,18 @@ import * as columnFactories from './columns'; import { componentEntityColumns, systemEntityColumns } from './presets'; import { Table, TableColumn } from '@backstage/core-components'; -type Props = { +/** + * Props for {@link EntityTable}. + * + * @public + */ +export interface EntityTableProps { title: string; variant?: 'gridItem'; entities: T[]; emptyContent?: ReactNode; columns: TableColumn[]; -}; +} const useStyles = makeStyles(theme => ({ empty: { @@ -37,13 +42,21 @@ const useStyles = makeStyles(theme => ({ }, })); -export function EntityTable({ - entities, - title, - emptyContent, - variant = 'gridItem', - columns, -}: Props) { +/** + * A general entity table component, that can be used for composing more + * specific entity tables. + * + * @public + */ +export function EntityTable(props: EntityTableProps) { + const { + entities, + title, + emptyContent, + variant = 'gridItem', + columns, + } = props; + const classes = useStyles(); const tableStyle: React.CSSProperties = { minWidth: '0', diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index 1f4f5ee0d0..f6198fa1dc 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -29,11 +29,11 @@ import { formatEntityRefTitle, } from '../EntityRefLink'; -export function createEntityRefColumn({ - defaultKind, -}: { +/** @public */ +export function createEntityRefColumn(options: { defaultKind?: string; }): TableColumn { + const { defaultKind } = options; function formatContent(entity: T): string { return ( entity.metadata?.title || @@ -49,7 +49,7 @@ export function createEntityRefColumn({ customFilterAndSearch(filter, entity) { // TODO: We could implement this more efficiently, like searching over // each field that is displayed individually (kind, namespace, name). - // but that migth confuse the user as it will behave different than a + // but that might confuse the user as it will behave different than a // simple text search. // Another alternative would be to cache the values. But writing them // into the entity feels bad too. @@ -70,6 +70,7 @@ export function createEntityRefColumn({ }; } +/** @public */ export function createEntityRelationColumn({ title, relation, @@ -110,6 +111,7 @@ export function createEntityRelationColumn({ }; } +/** @public */ export function createOwnerColumn(): TableColumn { return createEntityRelationColumn({ title: 'Owner', @@ -118,6 +120,7 @@ export function createOwnerColumn(): TableColumn { }); } +/** @public */ export function createDomainColumn(): TableColumn { return createEntityRelationColumn({ title: 'Domain', @@ -129,6 +132,7 @@ export function createDomainColumn(): TableColumn { }); } +/** @public */ export function createSystemColumn(): TableColumn { return createEntityRelationColumn({ title: 'System', @@ -140,6 +144,7 @@ export function createSystemColumn(): TableColumn { }); } +/** @public */ export function createMetadataDescriptionColumn< T extends Entity, >(): TableColumn { @@ -157,6 +162,7 @@ export function createMetadataDescriptionColumn< }; } +/** @public */ export function createSpecLifecycleColumn(): TableColumn { return { title: 'Lifecycle', @@ -164,6 +170,7 @@ export function createSpecLifecycleColumn(): TableColumn { }; } +/** @public */ export function createSpecTypeColumn(): TableColumn { return { title: 'Type', diff --git a/plugins/catalog-react/src/components/EntityTable/index.ts b/plugins/catalog-react/src/components/EntityTable/index.ts index 36203e7929..2ed07f9823 100644 --- a/plugins/catalog-react/src/components/EntityTable/index.ts +++ b/plugins/catalog-react/src/components/EntityTable/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { EntityTable } from './EntityTable'; +export type { EntityTableProps } from './EntityTable'; diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx index 770c187b4e..c6b985bc8b 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.test.tsx @@ -149,4 +149,34 @@ describe('', () => { tags: undefined, }); }); + + it('responds to external queryParameters changes', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: new EntityTagFilter(['tag1']), + }); + rendered.rerender( + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + tags: new EntityTagFilter(['tag2']), + }); + }); }); diff --git a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx index a25c50c0a9..a91389a653 100644 --- a/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx +++ b/plugins/catalog-react/src/components/EntityTagPicker/EntityTagPicker.tsx @@ -46,18 +46,29 @@ const useStyles = makeStyles( const icon = ; const checkedIcon = ; +/** @public */ export const EntityTagPicker = () => { const classes = useStyles(); const { updateFilters, backendEntities, filters, queryParameters } = useEntityListProvider(); - const queryParamTags = [queryParameters.tags] - .flat() - .filter(Boolean) as string[]; + const queryParamTags = useMemo( + () => [queryParameters.tags].flat().filter(Boolean) as string[], + [queryParameters], + ); + const [selectedTags, setSelectedTags] = useState( queryParamTags.length ? queryParamTags : filters.tags?.values ?? [], ); + // Set selected tags on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + if (queryParamTags.length) { + setSelectedTags(queryParamTags); + } + }, [queryParamTags]); + useEffect(() => { updateFilters({ tags: selectedTags.length ? new EntityTagFilter(selectedTags) : undefined, diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index f6e9d296df..9ed27bbef8 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -151,4 +151,38 @@ describe('', () => { type: new EntityTypeFilter(['tool']), }); }); + + it('responds to external queryParameters changes', async () => { + const updateFilters = jest.fn(); + const rendered = await renderWithEffects( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + type: new EntityTypeFilter(['service']), + }); + rendered.rerender( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + type: new EntityTypeFilter(['tool']), + }); + }); }); diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx index 225193dc93..a0770ef30a 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.tsx @@ -22,12 +22,18 @@ import { useEntityTypeFilter } from '../../hooks/useEntityTypeFilter'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { Select } from '@backstage/core-components'; -export type EntityTypeFilterProps = { +/** + * Props for {@link EntityTypePicker}. + * + * @public + */ +export interface EntityTypePickerProps { initialFilter?: string; hidden?: boolean; -}; +} -export const EntityTypePicker = (props: EntityTypeFilterProps) => { +/** @public */ +export const EntityTypePicker = (props: EntityTypePickerProps) => { const { hidden, initialFilter } = props; const alertApi = useApi(alertApiRef); const { error, availableTypes, selectedTypes, setSelectedTypes } = diff --git a/plugins/catalog-react/src/components/EntityTypePicker/index.ts b/plugins/catalog-react/src/components/EntityTypePicker/index.ts index 1124b5c30c..03351337b7 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/index.ts +++ b/plugins/catalog-react/src/components/EntityTypePicker/index.ts @@ -15,4 +15,4 @@ */ export { EntityTypePicker } from './EntityTypePicker'; -export type { EntityTypeFilterProps } from './EntityTypePicker'; +export type { EntityTypePickerProps } from './EntityTypePicker'; diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx new file mode 100644 index 0000000000..bfb5fd4179 --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx @@ -0,0 +1,162 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + makeStyles, + Tab, + Tabs, +} from '@material-ui/core'; +import React, { useEffect } from 'react'; +import { AncestryPage } from './components/AncestryPage'; +import { ColocatedPage } from './components/ColocatedPage'; +import { JsonPage } from './components/JsonPage'; +import { OverviewPage } from './components/OverviewPage'; +import { YamlPage } from './components/YamlPage'; + +const useStyles = makeStyles(theme => ({ + fullHeightDialog: { + height: 'calc(100% - 64px)', + }, + root: { + display: 'flex', + flexGrow: 1, + width: '100%', + backgroundColor: theme.palette.background.paper, + }, + tabs: { + borderRight: `1px solid ${theme.palette.divider}`, + flexShrink: 0, + }, + tabContents: { + flexGrow: 1, + overflowX: 'auto', + }, +})); + +function TabPanel(props: { + children?: React.ReactNode; + index: number; + value: number; +}) { + const { children, value, index, ...other } = props; + const classes = useStyles(); + return ( + + ); +} + +function a11yProps(index: number) { + return { + id: `vertical-tab-${index}`, + 'aria-controls': `vertical-tabpanel-${index}`, + }; +} + +/** + * A dialog that lets users inspect the low level details of their entities. + * + * @public + */ +export function InspectEntityDialog(props: { + open: boolean; + entity: Entity; + onClose: () => void; +}) { + const classes = useStyles(); + const [activeTab, setActiveTab] = React.useState(0); + + useEffect(() => { + setActiveTab(0); + }, [props.open]); + + if (!props.entity) { + return null; + } + + return ( + + + Entity Inspector + + +
+ setActiveTab(newValue)} + aria-label="Inspector options" + className={classes.tabs} + > + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ ); +} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx new file mode 100644 index 0000000000..b27bbe3948 --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx @@ -0,0 +1,222 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Entity, + ENTITY_DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { + DependencyGraph, + DependencyGraphTypes, + Link, + Progress, + ResponseErrorPanel, +} from '@backstage/core-components'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { Box, DialogContentText, makeStyles } from '@material-ui/core'; +import classNames from 'classnames'; +import React, { useLayoutEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router'; +import useAsync from 'react-use/lib/useAsync'; +import { catalogApiRef } from '../../../api'; +import { formatEntityRefTitle } from '../../../components/EntityRefLink/format'; +import { entityRouteRef } from '../../../routes'; +import { EntityKindIcon } from './EntityKindIcon'; + +const useStyles = makeStyles(theme => ({ + node: { + fill: theme.palette.grey[300], + stroke: theme.palette.grey[300], + '&.primary': { + fill: theme.palette.primary.light, + stroke: theme.palette.primary.light, + }, + '&.secondary': { + fill: theme.palette.secondary.light, + stroke: theme.palette.secondary.light, + }, + }, + text: { + fill: theme.palette.getContrastText(theme.palette.grey[300]), + '&.primary': { + fill: theme.palette.primary.contrastText, + }, + '&.secondary': { + fill: theme.palette.secondary.contrastText, + }, + '&.focused': { + fontWeight: 'bold', + }, + }, + clickable: { + cursor: 'pointer', + }, +})); + +type NodeType = Entity & { root: boolean }; + +function useAncestry(root: Entity): { + loading: boolean; + error?: Error; + nodes: DependencyGraphTypes.DependencyNode[]; + edges: DependencyGraphTypes.DependencyEdge[]; +} { + const catalogClient = useApi(catalogApiRef); + const entityRef = stringifyEntityRef(root); + + const { loading, error, value } = useAsync(async () => { + const response = await catalogClient.getEntityAncestors({ entityRef }); + const nodes = new Array>(); + const edges = new Array(); + for (const current of response.items) { + const currentRef = stringifyEntityRef(current.entity); + const isRootNode = currentRef === response.rootEntityRef; + nodes.push({ id: currentRef, root: isRootNode, ...current.entity }); + for (const parentRef of current.parentEntityRefs) { + edges.push({ from: currentRef, to: parentRef }); + } + } + return { nodes, edges }; + }, [entityRef]); + + return { + loading, + error, + nodes: value?.nodes || [], + edges: value?.edges || [], + }; +} + +function CustomNode({ node }: DependencyGraphTypes.RenderNodeProps) { + const classes = useStyles(); + const navigate = useNavigate(); + const entityRoute = useRouteRef(entityRouteRef); + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + const idRef = useRef(null); + + useLayoutEffect(() => { + // set the width to the length of the ID + if (idRef.current) { + let { height: renderedHeight, width: renderedWidth } = + idRef.current.getBBox(); + renderedHeight = Math.round(renderedHeight); + renderedWidth = Math.round(renderedWidth); + if (renderedHeight !== height || renderedWidth !== width) { + setWidth(renderedWidth); + setHeight(renderedHeight); + } + } + }, [width, height]); + + const padding = 10; + const iconSize = height; + const paddedIconWidth = iconSize + padding; + const paddedWidth = paddedIconWidth + width + padding * 2; + const paddedHeight = height + padding * 2; + + const displayTitle = + node.metadata.title || + (node.kind && node.metadata.name && node.metadata.namespace + ? formatEntityRefTitle({ + kind: node.kind, + name: node.metadata.name, + namespace: node.metadata.namespace || '', + }) + : node.id); + + const onClick = () => { + navigate( + entityRoute({ + kind: node.kind, + namespace: node.metadata.namespace || ENTITY_DEFAULT_NAMESPACE, + name: node.metadata.name, + }), + ); + }; + + return ( + + + + + {displayTitle} + + + ); +} + +export function AncestryPage(props: { entity: Entity }) { + const { loading, error, nodes, edges } = useAncestry(props.entity); + if (loading) { + return ; + } else if (error) { + return ; + } + + return ( + <> + Ancestry + + This is the ancestry of entities above the current one - as in, the + chain(s) of entities down to the current one, where{' '} + + processors emitted + {' '} + child entities that ultimately led to the current one existing. Note + that this is a completely different mechanism from relations. + + + + + + ); +} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx new file mode 100644 index 0000000000..d5a98f434c --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx @@ -0,0 +1,171 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Entity, + ANNOTATION_LOCATION, + ANNOTATION_ORIGIN_LOCATION, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { Progress, ResponseErrorPanel } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { + DialogContentText, + List, + ListItem, + ListItemIcon, + makeStyles, +} from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { catalogApiRef } from '../../../api'; +import { EntityRefLink } from '../../EntityRefLink'; +import { KeyValueListItem, ListItemText } from './common'; +import { EntityKindIcon } from './EntityKindIcon'; + +const useStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + }, +}); + +function useColocated(entity: Entity): { + loading: boolean; + error?: Error; + location?: string; + originLocation?: string; + colocatedEntities?: Entity[]; +} { + const catalogApi = useApi(catalogApiRef); + const currentEntityRef = stringifyEntityRef(entity); + const location = entity.metadata.annotations?.[ANNOTATION_LOCATION]; + const origin = entity.metadata.annotations?.[ANNOTATION_ORIGIN_LOCATION]; + + const { loading, error, value } = useAsync(async () => { + if (!location && !origin) { + return []; + } + const response = await catalogApi.getEntities({ + filter: [ + ...(location + ? [{ [`metadata.annotations.${ANNOTATION_LOCATION}`]: location }] + : []), + ...(origin + ? [{ [`metadata.annotations.${ANNOTATION_ORIGIN_LOCATION}`]: origin }] + : []), + ], + }); + return response.items; + }, [location, origin]); + + return { + loading, + error, + location, + originLocation: origin, + colocatedEntities: value?.filter( + colocated => stringifyEntityRef(colocated) !== currentEntityRef, + ), + }; +} + +function EntityList(props: { entities: Entity[]; header?: [string, string] }) { + return ( + + {props.header && } + {props.entities.map(entity => ( + + + + + } /> + + ))} + + ); +} + +function Contents(props: { entity: Entity }) { + const { entity } = props; + + const { loading, error, location, originLocation, colocatedEntities } = + useColocated(entity); + if (loading) { + return ; + } else if (error) { + return ; + } + + if (!location && !originLocation) { + return ( + Entity had no location information. + ); + } else if (!colocatedEntities?.length) { + return ( + + There were no other entities on this location. + + ); + } + + if (location === originLocation) { + return ; + } + + const atLocation = colocatedEntities.filter( + e => e.metadata.annotations?.[ANNOTATION_LOCATION] === location, + ); + const atOrigin = colocatedEntities.filter( + e => + e.metadata.annotations?.[ANNOTATION_ORIGIN_LOCATION] === originLocation, + ); + + return ( + <> + {atLocation.length > 0 && ( + + )} + {atOrigin.length > 0 && ( + + )} + + ); +} + +export function ColocatedPage(props: { entity: Entity }) { + const classes = useStyles(); + return ( + <> + Colocated + + These are the entities that are colocated with this entity - as in, they + originated from the same data source (e.g. came from the same YAML + file), or from the same origin (e.g. the originally registered URL). + +
+ +
+ + ); +} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/EntityKindIcon.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/EntityKindIcon.tsx new file mode 100644 index 0000000000..c2fc49aa0c --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/EntityKindIcon.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useApp } from '@backstage/core-plugin-api'; +import WorkIcon from '@material-ui/icons/Work'; +import React from 'react'; + +export function EntityKindIcon(props: { + kind: string; + x?: number; + y?: number; + width?: number; + height?: number; + className?: string; +}) { + const app = useApp(); + + const { kind, ...otherProps } = props; + const Icon = + app.getSystemIcon(`kind:${kind.toLocaleLowerCase('en-US')}`) ?? WorkIcon; + + return ; +} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.test.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.test.tsx new file mode 100644 index 0000000000..83888a867f --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentEntity } from '@backstage/catalog-model'; +import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { JsonPage } from './JsonPage'; + +describe('JsonPage', () => { + it('renders', async () => { + const entity: ComponentEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'c1', + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'ops', + }, + }; + + await renderInTestApp(); + + expect(screen.getByTestId('code-snippet')).toHaveTextContent( + '"lifecycle": "production"', + ); + }); +}); diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx new file mode 100644 index 0000000000..44dd626963 --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { CodeSnippet } from '@backstage/core-components'; +import { DialogContentText } from '@material-ui/core'; +import React from 'react'; +import { sortKeys } from './util'; + +export function JsonPage(props: { entity: Entity }) { + return ( + <> + Entity as JSON + + This is the raw entity data as received from the catalog, on JSON form. + + +
+ +
+
+ + ); +} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx new file mode 100644 index 0000000000..dbb7399481 --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx @@ -0,0 +1,180 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AlphaEntity, stringifyEntityRef } from '@backstage/catalog-model'; +import { + Box, + DialogContentText, + List, + ListItem, + ListItemIcon, + makeStyles, + Typography, +} from '@material-ui/core'; +import groupBy from 'lodash/groupBy'; +import sortBy from 'lodash/sortBy'; +import React from 'react'; +import { EntityRefLink } from '../../EntityRefLink'; +import { + Container, + HelpIcon, + KeyValueListItem, + ListItemText, + ListSubheader, +} from './common'; +import { EntityKindIcon } from './EntityKindIcon'; + +const useStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + }, +}); + +export function OverviewPage(props: { entity: AlphaEntity }) { + const classes = useStyles(); + const { + apiVersion, + kind, + metadata, + spec, + relations = [], + status = {}, + } = props.entity; + + const groupedRelations = groupBy( + sortBy(relations, r => stringifyEntityRef(r.target)), + 'type', + ); + + return ( + <> + Overview +
+ + + + + + + + + {spec?.type && ( + + + + )} + {metadata.uid && ( + + + + )} + {metadata.etag && ( + + + + )} + + + + + {!!Object.keys(metadata.annotations || {}).length && ( + + Annotations + + + } + > + {Object.entries(metadata.annotations!).map(entry => ( + + ))} + + )} + {!!Object.keys(metadata.labels || {}).length && ( + + Labels + + + } + > + {Object.entries(metadata.labels!).map(entry => ( + + ))} + + )} + {!!metadata.tags?.length && ( + Tags}> + {metadata.tags.map((tag, index) => ( + + + + + ))} + + )} + + + {!!relations.length && ( + + {Object.entries(groupedRelations).map( + ([type, groupRelations], index) => ( +
+ {type}}> + {groupRelations.map(group => ( + + + + + } + /> + + ))} + +
+ ), + )} +
+ )} + + {!!status.items?.length && ( + + {status.items.map((item, index) => ( +
+ + {item.level}: {item.type} + + {item.message} +
+ ))} +
+ )} +
+ + ); +} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.test.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.test.tsx new file mode 100644 index 0000000000..8106197301 --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentEntity } from '@backstage/catalog-model'; +import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { YamlPage } from './YamlPage'; + +describe('YamlPage', () => { + it('renders', async () => { + const entity: ComponentEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'c1', + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'ops', + }, + }; + + await renderInTestApp(); + + expect(screen.getByTestId('code-snippet')).toHaveTextContent( + 'lifecycle: production', + ); + }); +}); diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx new file mode 100644 index 0000000000..ad74611ec5 --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { CodeSnippet } from '@backstage/core-components'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import React from 'react'; +import YAML from 'yaml'; +import { sortKeys } from './util'; + +export function YamlPage(props: { entity: Entity }) { + return ( + <> + Entity as YAML + + This is the raw entity data as received from the catalog, on YAML form. + + +
+ +
+
+ + ); +} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/common.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/common.tsx new file mode 100644 index 0000000000..79a3c69955 --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/common.tsx @@ -0,0 +1,124 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Link } from '@backstage/core-components'; +import { + Box, + Card, + CardContent, + ListItem, + ListItemIcon, + ListItemText as MuiListItemText, + ListSubheader as MuiListSubheader, + makeStyles, + Typography, +} from '@material-ui/core'; +import HelpOutlineIcon from '@material-ui/icons/HelpOutline'; +import React from 'react'; + +const useStyles = makeStyles(theme => ({ + root: { + display: 'flex', + flexDirection: 'column', + }, + marginTop: { + marginTop: theme.spacing(2), + }, + helpIcon: { + marginLeft: theme.spacing(1), + color: theme.palette.text.disabled, + }, + monospace: { + fontFamily: 'monospace', + }, +})); + +export function ListItemText(props: { + primary: React.ReactNode; + secondary?: React.ReactNode; +}) { + const classes = useStyles(); + return ( + + ); +} + +export function ListSubheader(props: { children?: React.ReactNode }) { + const classes = useStyles(); + return ( + + {props.children} + + ); +} + +export function Container(props: { + title: React.ReactNode; + helpLink?: string; + children: React.ReactNode; +}) { + return ( + + + + + {props.title} + {props.helpLink && } + + {props.children} + + + + ); +} + +export function KeyValueListItem(props: { + indent?: boolean; + entry: [string, string]; +}) { + const [key, value] = props.entry; + return ( + + {props.indent && } + + {value.substring(0, 4)} + {value.substring(4)} + + ) + } + /> + + ); +} + +export function HelpIcon(props: { to: string }) { + const classes = useStyles(); + return ( + + + + ); +} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/util.ts b/plugins/catalog-react/src/components/InspectEntityDialog/components/util.ts new file mode 100644 index 0000000000..9dd25ba538 --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/util.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JsonObject } from '@backstage/types'; + +export function sortKeys(data: JsonObject): JsonObject { + // we could do something custom, but lexicographical sorting is actually a + // good choice at least for the default set of keys + return Object.fromEntries( + [...Object.entries(data)].sort((a, b) => (a[0] < b[0] ? -1 : 1)), + ); +} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/index.ts b/plugins/catalog-react/src/components/InspectEntityDialog/index.ts new file mode 100644 index 0000000000..1360ade125 --- /dev/null +++ b/plugins/catalog-react/src/components/InspectEntityDialog/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { InspectEntityDialog } from './InspectEntityDialog'; diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx index 2b29bba4fd..68cb5f2bdf 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx @@ -19,7 +19,7 @@ jest.mock('./useUnregisterEntityDialogState'); import userEvent from '@testing-library/user-event'; import React from 'react'; import { UnregisterEntityDialog } from './UnregisterEntityDialog'; -import { ORIGIN_LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import { ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model'; import { CatalogClient } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; import { entityRouteRef } from '../../routes'; @@ -55,7 +55,7 @@ describe('UnregisterEntityDialog', () => { name: 'n', namespace: 'ns', annotations: { - [ORIGIN_LOCATION_ANNOTATION]: 'url:http://example.com', + [ANNOTATION_ORIGIN_LOCATION]: 'url:http://example.com', }, }, spec: {}, diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx index 8d6083f111..44fc3886ea 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx @@ -14,12 +14,8 @@ * limitations under the License. */ -import { - Entity, - Location, - ORIGIN_LOCATION_ANNOTATION, -} from '@backstage/catalog-model'; -import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogApi, Location } from '@backstage/catalog-client'; +import { Entity, ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model'; import { catalogApiRef } from '../../api'; import { act, @@ -83,7 +79,7 @@ describe('useUnregisterEntityDialogState', () => { name: 'n', namespace: 'ns', annotations: { - [ORIGIN_LOCATION_ANNOTATION]: 'url:https://example.com', + [ANNOTATION_ORIGIN_LOCATION]: 'url:https://example.com', }, }, spec: {}, @@ -117,7 +113,7 @@ describe('useUnregisterEntityDialogState', () => { }); it('chooses the bootstrap path when necessary', async () => { - entity.metadata.annotations![ORIGIN_LOCATION_ANNOTATION] = + entity.metadata.annotations![ANNOTATION_ORIGIN_LOCATION] = 'bootstrap:bootstrap'; let rendered: RenderHookResult; @@ -141,7 +137,7 @@ describe('useUnregisterEntityDialogState', () => { }); it('chooses only-delete when there was no location annotation', async () => { - delete entity.metadata.annotations![ORIGIN_LOCATION_ANNOTATION]; + delete entity.metadata.annotations![ANNOTATION_ORIGIN_LOCATION]; let rendered: RenderHookResult; act(() => { diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts index 7f75016e16..c637490268 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts @@ -18,7 +18,7 @@ import { Entity, EntityName, getEntityName, - ORIGIN_LOCATION_ANNOTATION, + ANNOTATION_ORIGIN_LOCATION, } from '@backstage/catalog-model'; import { catalogApiRef } from '../../api'; import { useCallback } from 'react'; @@ -60,7 +60,7 @@ export function useUnregisterEntityDialogState( entity: Entity, ): UseUnregisterEntityDialogState { const catalogApi = useApi(catalogApiRef); - const locationRef = entity.metadata.annotations?.[ORIGIN_LOCATION_ANNOTATION]; + const locationRef = entity.metadata.annotations?.[ANNOTATION_ORIGIN_LOCATION]; const uid = entity.metadata.uid; const isBootstrap = locationRef === 'bootstrap:bootstrap'; @@ -73,7 +73,7 @@ export function useUnregisterEntityDialogState( if (!locationRef) { colocatedEntitiesPromise = Promise.resolve([]); } else { - const locationAnnotationFilter = `metadata.annotations.${ORIGIN_LOCATION_ANNOTATION}`; + const locationAnnotationFilter = `metadata.annotations.${ANNOTATION_ORIGIN_LOCATION}`; colocatedEntitiesPromise = catalogApi .getEntities({ filter: { [locationAnnotationFilter]: locationRef }, diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index f6a4c59374..d4cfedecbf 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -69,11 +69,13 @@ const apis = TestApiRegistry.from( [storageApiRef, MockStorageApi.create()], ); -const mockIsOwnedEntity = (entity: Entity) => - entity.metadata.name === 'component-1'; +const mockIsOwnedEntity = jest.fn( + (entity: Entity) => entity.metadata.name === 'component-1', +); -const mockIsStarredEntity = (entity: Entity) => - entity.metadata.name === 'component-3'; +const mockIsStarredEntity = jest.fn( + (entity: Entity) => entity.metadata.name === 'component-3', +); jest.mock('../../hooks', () => { const actual = jest.requireActual('../../hooks'); @@ -250,4 +252,122 @@ describe('', () => { ), }); }); + + it('responds to external queryParameters changes', () => { + const updateFilters = jest.fn(); + const rendered = render( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter('all', mockIsOwnedEntity, mockIsStarredEntity), + }); + rendered.rerender( + + + + + , + ); + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter('owned', mockIsOwnedEntity, mockIsStarredEntity), + }); + }); + + describe.each` + type | filterFn + ${'owned'} | ${mockIsOwnedEntity} + ${'starred'} | ${mockIsStarredEntity} + `('filter resetting for $type entities', ({ type, filterFn }) => { + let updateFilters: jest.Mock; + + const picker = ({ loading }: { loading: boolean }) => ( + + + + + + ); + + beforeEach(() => { + updateFilters = jest.fn(); + }); + + describe(`when there are no ${type} entities match the filter`, () => { + beforeEach(() => { + filterFn.mockReturnValue(false); + }); + + it('does not reset the filter while entities are loading', () => { + render(picker({ loading: true })); + + expect(updateFilters).not.toHaveBeenCalledWith({ + user: new UserListFilter( + 'all', + mockIsOwnedEntity, + mockIsStarredEntity, + ), + }); + }); + + it('resets the filter to "all" when entities are loaded', () => { + render(picker({ loading: false })); + + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter( + 'all', + mockIsOwnedEntity, + mockIsStarredEntity, + ), + }); + }); + }); + + describe(`when there are some ${type} entities present`, () => { + beforeEach(() => { + filterFn.mockReturnValue(true); + }); + + it('does not reset the filter while entities are loading', () => { + render(picker({ loading: true })); + + expect(updateFilters).not.toHaveBeenCalledWith({ + user: new UserListFilter( + 'all', + mockIsOwnedEntity, + mockIsStarredEntity, + ), + }); + }); + + it('does not reset the filter when entities are loaded', () => { + render(picker({ loading: false })); + + expect(updateFilters).toHaveBeenLastCalledWith({ + user: new UserListFilter( + type, + mockIsOwnedEntity, + mockIsStarredEntity, + ), + }); + }); + }); + }); }); diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index 8746752b08..cbf6089f2e 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -130,7 +130,7 @@ export const UserListPicker = ({ const classes = useStyles(); const configApi = useApi(configApiRef); const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; - const { filters, updateFilters, backendEntities, queryParameters } = + const { filters, updateFilters, backendEntities, queryParameters, loading } = useEntityListProvider(); // Remove group items that aren't in availableFilters and exclude @@ -161,19 +161,59 @@ export const UserListPicker = ({ [isOwnedEntity, isStarredEntity], ); + const queryParamUserFilter = useMemo( + () => [queryParameters.user].flat()[0], + [queryParameters], + ); + + const [selectedUserFilter, setSelectedUserFilter] = useState( + queryParamUserFilter ?? initialFilter, + ); + // To show proper counts for each section, apply all other frontend filters _except_ the user // filter that's controlled by this picker. - const [entitiesWithoutUserFilter, setEntitiesWithoutUserFilter] = - useState(backendEntities); - const totalOwnedUserEntities = entitiesWithoutUserFilter.filter(entity => - ownedFilter.filterEntity(entity), - ).length; - const [selectedUserFilter, setSelectedUserFilter] = useState( - totalOwnedUserEntities > 0 - ? [queryParameters.user].flat()[0] ?? initialFilter - : 'all', + const entitiesWithoutUserFilter = useMemo( + () => + backendEntities.filter( + reduceEntityFilters( + compact(Object.values({ ...filters, user: undefined })), + ), + ), + [filters, backendEntities], ); + const filterCounts = useMemo>( + () => ({ + all: entitiesWithoutUserFilter.length, + starred: entitiesWithoutUserFilter.filter(entity => + starredFilter.filterEntity(entity), + ).length, + owned: entitiesWithoutUserFilter.filter(entity => + ownedFilter.filterEntity(entity), + ).length, + }), + [entitiesWithoutUserFilter, starredFilter, ownedFilter], + ); + + // Set selected user filter on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + if (queryParamUserFilter) { + setSelectedUserFilter(queryParamUserFilter as UserListFilterKind); + } + }, [queryParamUserFilter]); + + useEffect(() => { + if ( + !loading && + !!selectedUserFilter && + selectedUserFilter !== 'all' && + filterCounts[selectedUserFilter] === 0 + ) { + setSelectedUserFilter('all'); + } + }, [loading, filterCounts, selectedUserFilter, setSelectedUserFilter]); + useEffect(() => { updateFilters({ user: selectedUserFilter @@ -186,26 +226,6 @@ export const UserListPicker = ({ }); }, [selectedUserFilter, isOwnedEntity, isStarredEntity, updateFilters]); - useEffect(() => { - const filterFn = reduceEntityFilters( - compact(Object.values({ ...filters, user: undefined })), - ); - setEntitiesWithoutUserFilter(backendEntities.filter(filterFn)); - }, [filters, backendEntities]); - - function getFilterCount(id: UserListFilterKind) { - switch (id) { - case 'owned': - return totalOwnedUserEntities; - case 'starred': - return entitiesWithoutUserFilter.filter(entity => - starredFilter.filterEntity(entity), - ).length; - default: - return entitiesWithoutUserFilter.length; - } - } - return ( {filterGroups.map(group => ( @@ -223,6 +243,8 @@ export const UserListPicker = ({ onClick={() => setSelectedUserFilter(item.id)} selected={item.id === filters.user?.value} className={classes.menuItem} + disabled={filterCounts[item.id] === 0} + data-testid={`user-picker-${item.id}`} > {item.icon && ( @@ -230,15 +252,10 @@ export const UserListPicker = ({ )} - - {item.label} - + {item.label} - {getFilterCount(item.id) ?? '-'} + {filterCounts[item.id] ?? '-'} ))} diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index 0977e86d58..807d6bb702 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export * from './EntityKindPicker'; export * from './EntityLifecyclePicker'; export * from './EntityOwnerPicker'; @@ -22,5 +23,6 @@ export * from './EntityTable'; export * from './EntityTagPicker'; export * from './EntityTypePicker'; export * from './FavoriteEntity'; +export * from './InspectEntityDialog'; export * from './UnregisterEntityDialog'; export * from './UserListPicker'; diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index a1e722bc6d..548561108d 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -27,6 +27,7 @@ import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import qs from 'qs'; import React, { PropsWithChildren } from 'react'; +import { MemoryRouter } from 'react-router'; import { catalogApiRef } from '../api'; import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis'; import { EntityKindPicker, UserListPicker } from '../components'; @@ -82,31 +83,35 @@ const mockCatalogApi: Partial = { const wrapper = ({ userFilter, + location, children, }: PropsWithChildren<{ userFilter?: UserListFilterKind; + location?: string; }>) => { return ( - - - - + + + + + + ); }; @@ -168,10 +173,11 @@ describe('', () => { const query = qs.stringify({ filters: { kind: 'component', type: 'service' }, }); - delete (window as any).location; - (window as any).location = new URL(`http://localhost/catalog?${query}`); const { result, waitFor } = renderHook(() => useEntityListProvider(), { wrapper, + initialProps: { + location: `/catalog?${query}`, + }, }); await waitFor(() => !!result.current.queryParameters); expect(result.current.queryParameters).toEqual({ diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index fb8eb7279f..9b4d850208 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -25,6 +25,7 @@ import React, { useMemo, useState, } from 'react'; +import { useLocation } from 'react-router'; import useAsyncFn from 'react-use/lib/useAsyncFn'; import useDebounce from 'react-use/lib/useDebounce'; import useMountedState from 'react-use/lib/useMountedState'; @@ -98,7 +99,6 @@ type OutputState = { appliedFilters: EntityFilters; entities: Entity[]; backendEntities: Entity[]; - queryParameters: Record; }; export const EntityListProvider = ({ @@ -109,19 +109,26 @@ export const EntityListProvider = ({ const [requestedFilters, setRequestedFilters] = useState( {} as EntityFilters, ); + + // We use react-router's useLocation hook so updates from external sources trigger an update to + // the queryParameters in outputState. Updates from this hook use replaceState below and won't + // trigger a useLocation change; this would instead come from an external source, such as a manual + // update of the URL or two catalog sidebar links with different catalog filters. + const location = useLocation(); + const queryParameters = useMemo( + () => + (qs.parse(location.search, { + ignoreQueryPrefix: true, + }).filters ?? {}) as Record, + [location], + ); + const [outputState, setOutputState] = useState>( () => { - const query = qs.parse(window.location.search, { - ignoreQueryPrefix: true, - }); return { appliedFilters: {} as EntityFilters, entities: [], backendEntities: [], - queryParameters: (query.filters ?? {}) as Record< - string, - string | string[] - >, }; }, ); @@ -163,19 +170,17 @@ export const EntityListProvider = ({ appliedFilters: requestedFilters, backendEntities: response.items, entities: response.items.filter(entityFilter), - queryParameters: queryParams, }); } else { setOutputState({ appliedFilters: requestedFilters, backendEntities: outputState.backendEntities, entities: outputState.backendEntities.filter(entityFilter), - queryParameters: queryParams, }); } if (isMounted()) { - const oldParams = qs.parse(window.location.search, { + const oldParams = qs.parse(location.search, { ignoreQueryPrefix: true, }); const newParams = qs.stringify( @@ -191,7 +196,7 @@ export const EntityListProvider = ({ window.history?.replaceState(null, document.title, newUrl); } }, - [catalogApi, requestedFilters, outputState], + [catalogApi, queryParameters, requestedFilters, outputState], { loading: true }, ); @@ -220,11 +225,11 @@ export const EntityListProvider = ({ entities: outputState.entities, backendEntities: outputState.backendEntities, updateFilters, - queryParameters: outputState.queryParameters, + queryParameters, loading, error, }), - [outputState, updateFilters, loading, error], + [outputState, updateFilters, queryParameters, loading, error], ); return ( diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx index 5c0139dd26..c4531d9117 100644 --- a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -42,13 +42,23 @@ export function useEntityTypeFilter(): EntityTypeReturn { updateFilters, } = useEntityListProvider(); - const queryParamTypes = [queryParameters.type] - .flat() - .filter(Boolean) as string[]; + const queryParamTypes = useMemo( + () => [queryParameters.type].flat().filter(Boolean) as string[], + [queryParameters], + ); + const [selectedTypes, setSelectedTypes] = useState( queryParamTypes.length ? queryParamTypes : typeFilter?.getTypes() ?? [], ); + // Set selected types on query parameter updates; this happens at initial page load and from + // external updates to the page location. + useEffect(() => { + if (queryParamTypes.length) { + setSelectedTypes(queryParamTypes); + } + }, [queryParamTypes]); + const [availableTypes, setAvailableTypes] = useState([]); const kind = useMemo(() => kindFilter?.value, [kindFilter]); diff --git a/plugins/catalog-react/src/hooks/useOwnedEntities.ts b/plugins/catalog-react/src/hooks/useOwnedEntities.ts index ffe64830f4..dad73740c1 100644 --- a/plugins/catalog-react/src/hooks/useOwnedEntities.ts +++ b/plugins/catalog-react/src/hooks/useOwnedEntities.ts @@ -19,8 +19,8 @@ import { loadIdentityOwnerRefs, } from './useEntityOwnership'; import { identityApiRef, useApi } from '@backstage/core-plugin-api'; -import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { CatalogListResponse } from '@backstage/catalog-client'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { GetEntitiesResponse } from '@backstage/catalog-client'; import useAsync from 'react-use/lib/useAsync'; import { useMemo } from 'react'; @@ -32,11 +32,10 @@ import { useMemo } from 'react'; * @public * * @param allowedKinds - Array of allowed kinds to filter the entities - * @returns CatalogListResponse */ export function useOwnedEntities(allowedKinds?: string[]): { loading: boolean; - ownedEntities: CatalogListResponse | undefined; + ownedEntities: GetEntitiesResponse | undefined; } { const identityApi = useApi(identityApiRef); const catalogApi = useApi(catalogApiRef); diff --git a/plugins/catalog-react/src/testUtils/providers.tsx b/plugins/catalog-react/src/testUtils/providers.tsx index 2172f6c5a9..3fb295298c 100644 --- a/plugins/catalog-react/src/testUtils/providers.tsx +++ b/plugins/catalog-react/src/testUtils/providers.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ -import React, { PropsWithChildren, useCallback, useState } from 'react'; +import React, { + PropsWithChildren, + useCallback, + useMemo, + useState, +} from 'react'; import { DefaultEntityFilters, EntityListContext, @@ -32,6 +37,7 @@ export const MockEntityListContextProvider = ({ const [filters, setFilters] = useState( value?.filters ?? {}, ); + const updateFilters = useCallback( ( update: @@ -49,23 +55,32 @@ export const MockEntityListContextProvider = ({ [], ); - const defaultContext: EntityListContextProps = { - entities: [], - backendEntities: [], - updateFilters, - filters, - loading: false, - queryParameters: {}, - }; + // Memoize the default values since pickers have useEffect triggers on these; naively defaulting + // below with `?? ` breaks referential equality on subsequent updates. + const defaultValues = useMemo( + () => ({ + entities: [], + backendEntities: [], + queryParameters: {}, + }), + [], + ); - // Extract value.filters to avoid overwriting it; some tests exercise filter updates. The value - // provided is used as the initial seed in useState above. - const { filters: _, ...otherContextFields } = value ?? {}; + const resolvedValue: EntityListContextProps = useMemo( + () => ({ + entities: value?.entities ?? defaultValues.entities, + backendEntities: value?.backendEntities ?? defaultValues.backendEntities, + updateFilters: value?.updateFilters ?? updateFilters, + filters, + loading: value?.loading ?? false, + queryParameters: value?.queryParameters ?? defaultValues.queryParameters, + error: value?.error, + }), + [value, defaultValues, filters, updateFilters], + ); return ( - + {children} ); diff --git a/plugins/catalog-react/src/utils/getEntityMetadataUrl.ts b/plugins/catalog-react/src/utils/getEntityMetadataUrl.ts index 996f928ecf..3b44816ac1 100644 --- a/plugins/catalog-react/src/utils/getEntityMetadataUrl.ts +++ b/plugins/catalog-react/src/utils/getEntityMetadataUrl.ts @@ -15,15 +15,15 @@ */ import { - EDIT_URL_ANNOTATION, + ANNOTATION_EDIT_URL, + ANNOTATION_VIEW_URL, Entity, - VIEW_URL_ANNOTATION, } from '@backstage/catalog-model'; export function getEntityMetadataViewUrl(entity: Entity): string | undefined { - return entity.metadata.annotations?.[VIEW_URL_ANNOTATION]; + return entity.metadata.annotations?.[ANNOTATION_VIEW_URL]; } export function getEntityMetadataEditUrl(entity: Entity): string | undefined { - return entity.metadata.annotations?.[EDIT_URL_ANNOTATION]; + return entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; } diff --git a/plugins/catalog-react/src/utils/getEntitySourceLocation.ts b/plugins/catalog-react/src/utils/getEntitySourceLocation.ts index 59c3b5ab95..dd4d5c07f6 100644 --- a/plugins/catalog-react/src/utils/getEntitySourceLocation.ts +++ b/plugins/catalog-react/src/utils/getEntitySourceLocation.ts @@ -15,9 +15,9 @@ */ import { + ANNOTATION_SOURCE_LOCATION, Entity, - parseLocationReference, - SOURCE_LOCATION_ANNOTATION, + parseLocationRef, } from '@backstage/catalog-model'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -31,14 +31,14 @@ export function getEntitySourceLocation( scmIntegrationsApi: ScmIntegrationRegistry, ): EntitySourceLocation | undefined { const sourceLocation = - entity.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION]; + entity.metadata.annotations?.[ANNOTATION_SOURCE_LOCATION]; if (!sourceLocation) { return undefined; } try { - const sourceLocationRef = parseLocationReference(sourceLocation); + const sourceLocationRef = parseLocationRef(sourceLocation); const integration = scmIntegrationsApi.byUrl(sourceLocationRef.target); return { locationTargetUrl: sourceLocationRef.target, diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index bcbfdf4fc2..cb13e27bd0 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-catalog +## 0.7.12 + +### Patch Changes + +- f8633307c4: Added an "inspect" entry in the entity three-dots menu, for lower level catalog + insights and debugging. +- 9033775d39: Deprecated the `EntityPageLayout`; please use the new extension based `CatalogEntityPage` instead +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + - @backstage/integration-react@0.1.21 + +## 0.7.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + - @backstage/integration-react@0.1.21-next.0 + +## 0.7.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + - @backstage/plugin-catalog-common@0.1.2 + - @backstage/integration-react@0.1.20 + +## 0.7.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + - @backstage/plugin-catalog-react@0.6.13-next.1 + - @backstage/plugin-catalog-common@0.1.2-next.0 + +## 0.7.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/integration-react@0.1.20-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.7.10 ### Patch Changes diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index ad391b594f..d526d4dee1 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -11,18 +11,18 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogClient } from '@backstage/catalog-client'; -import { CatalogEntitiesRequest } from '@backstage/catalog-client'; -import { CatalogEntityAncestorsRequest } from '@backstage/catalog-client'; -import { CatalogEntityAncestorsResponse } from '@backstage/catalog-client'; -import { CatalogListResponse } from '@backstage/catalog-client'; import { CatalogRequestOptions } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { EntityName } from '@backstage/catalog-model'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { GetEntitiesRequest } from '@backstage/catalog-client'; +import { GetEntitiesResponse } from '@backstage/catalog-client'; +import { GetEntityAncestorsRequest } from '@backstage/catalog-client'; +import { GetEntityAncestorsResponse } from '@backstage/catalog-client'; import { IconComponent } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; -import { Location as Location_2 } from '@backstage/catalog-model'; +import { Location as Location_2 } from '@backstage/catalog-client'; import { Overrides } from '@material-ui/core/styles/overrides'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; @@ -76,14 +76,14 @@ export class CatalogClientWrapper implements CatalogApi { ): Promise; // (undocumented) getEntities( - request?: CatalogEntitiesRequest, + request?: GetEntitiesRequest, options?: CatalogRequestOptions, - ): Promise>; + ): Promise; // (undocumented) getEntityAncestors( - request: CatalogEntityAncestorsRequest, + request: GetEntityAncestorsRequest, options?: CatalogRequestOptions, - ): Promise; + ): Promise; // (undocumented) getEntityByName( compoundName: EntityName, @@ -351,7 +351,7 @@ export const EntityOrphanWarning: () => JSX.Element; // Warning: (ae-missing-release-tag) "EntityPageLayout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public @deprecated export const EntityPageLayout: { ({ children, @@ -388,12 +388,6 @@ export const EntitySwitch: { }) => null; }; -// Warning: (ae-forgotten-export) The symbol "SystemDiagramCard" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "EntitySystemDiagramCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public @deprecated (undocumented) -export const EntitySystemDiagramCard: SystemDiagramCard; - // Warning: (ae-missing-release-tag) "FilterContainer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -481,6 +475,6 @@ export type SystemDiagramCardClassKey = // src/components/CatalogTable/CatalogTable.d.ts:11:5 - (ae-forgotten-export) The symbol "columnFactories" needs to be exported by the entry point index.d.ts // src/components/EntityLayout/EntityLayout.d.ts:43:5 - (ae-forgotten-export) The symbol "EntityLayoutProps" needs to be exported by the entry point index.d.ts // src/components/EntityLayout/EntityLayout.d.ts:44:5 - (ae-forgotten-export) The symbol "SubRoute" needs to be exported by the entry point index.d.ts -// src/components/EntityPageLayout/EntityPageLayout.d.ts:17:5 - (ae-forgotten-export) The symbol "EntityPageLayoutProps" needs to be exported by the entry point index.d.ts +// src/components/EntityPageLayout/EntityPageLayout.d.ts:22:5 - (ae-forgotten-export) The symbol "EntityPageLayoutProps" needs to be exported by the entry point index.d.ts // src/plugin.d.ts:22:5 - (ae-forgotten-export) The symbol "ColumnBreakpoints" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 066b2d99f5..a2492aaba7 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "0.7.10", + "version": "0.7.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,14 +31,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.5.5", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", - "@backstage/integration-react": "^0.1.19", - "@backstage/plugin-catalog-common": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/integration-react": "^0.1.21", + "@backstage/plugin-catalog-common": "^0.1.2", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -54,16 +54,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", "@backstage/plugin-permission-react": "^0.3.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" }, "files": [ "dist" diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts index 939c4f095b..b54ac0ed61 100644 --- a/plugins/catalog/src/CatalogClientWrapper.ts +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -14,17 +14,18 @@ * limitations under the License. */ -import { Entity, EntityName, Location } from '@backstage/catalog-model'; +import { Entity, EntityName } from '@backstage/catalog-model'; import { AddLocationRequest, AddLocationResponse, CatalogApi, CatalogClient, - CatalogEntitiesRequest, - CatalogListResponse, + GetEntitiesRequest, + GetEntitiesResponse, CatalogRequestOptions, - CatalogEntityAncestorsRequest, - CatalogEntityAncestorsResponse, + GetEntityAncestorsRequest, + GetEntityAncestorsResponse, + Location, } from '@backstage/catalog-client'; import { IdentityApi } from '@backstage/core-plugin-api'; @@ -58,9 +59,9 @@ export class CatalogClientWrapper implements CatalogApi { } async getEntities( - request?: CatalogEntitiesRequest, + request?: GetEntitiesRequest, options?: CatalogRequestOptions, - ): Promise> { + ): Promise { return await this.client.getEntities( request, await this.getCredentials(options), @@ -138,9 +139,9 @@ export class CatalogClientWrapper implements CatalogApi { } async getEntityAncestors( - request: CatalogEntityAncestorsRequest, + request: GetEntityAncestorsRequest, options?: CatalogRequestOptions, - ): Promise { + ): Promise { return await this.client.getEntityAncestors( request, await this.getCredentials(options), diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index a6c9635db2..cb6e057e61 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -17,7 +17,7 @@ import { Entity, ENTITY_DEFAULT_NAMESPACE, - LOCATION_ANNOTATION, + ANNOTATION_LOCATION, stringifyEntityRef, } from '@backstage/catalog-model'; import { @@ -127,7 +127,7 @@ export function AboutCard({ variant }: AboutCardProps) { cardContentClass = classes.fullHeightCardContent; } - const entityLocation = entity.metadata.annotations?.[LOCATION_ANNOTATION]; + const entityLocation = entity.metadata.annotations?.[ANNOTATION_LOCATION]; // Limiting the ability to manually refresh to the less expensive locations const allowRefresh = entityLocation?.startsWith('url:') || entityLocation?.startsWith('file:'); diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index 135a2a590d..20acff3c50 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -263,10 +263,12 @@ describe('DefaultCatalogPage', () => { const { getByTestId } = await renderWrapped(); fireEvent.click(getByTestId('user-picker-owned')); await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); - fireEvent.click(screen.getByTestId('user-picker-starred')); - await expect( - screen.findByText(/Starred \(0\)/), - ).resolves.toBeInTheDocument(); + // The "Starred" menu option should initially be disabled, since there + // aren't any starred entities. + await expect(screen.getByTestId('user-picker-starred')).toHaveAttribute( + 'aria-disabled', + 'true', + ); fireEvent.click(screen.getByTestId('user-picker-all')); await expect(screen.findByText(/All \(2\)/)).resolves.toBeInTheDocument(); @@ -274,6 +276,12 @@ describe('DefaultCatalogPage', () => { fireEvent.click(starredIcons[0]); await expect(screen.findByText(/All \(2\)/)).resolves.toBeInTheDocument(); + // Now that we've starred an entity, the "Starred" menu option should be + // enabled. + await expect(screen.getByTestId('user-picker-starred')).not.toHaveAttribute( + 'aria-disabled', + 'true', + ); fireEvent.click(screen.getByTestId('user-picker-starred')); await expect( screen.findByText(/Starred \(1\)/), diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 4c6adcf400..db9835e3a3 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -15,9 +15,9 @@ */ import { - EDIT_URL_ANNOTATION, + ANNOTATION_EDIT_URL, + ANNOTATION_VIEW_URL, Entity, - VIEW_URL_ANNOTATION, } from '@backstage/catalog-model'; import { ApiProvider } from '@backstage/core-app-api'; import { @@ -123,7 +123,7 @@ describe('CatalogTable component', () => { kind: 'Component', metadata: { name: 'component1', - annotations: { [EDIT_URL_ANNOTATION]: 'https://other.place' }, + annotations: { [ANNOTATION_EDIT_URL]: 'https://other.place' }, }, }; @@ -155,7 +155,7 @@ describe('CatalogTable component', () => { kind: 'Component', metadata: { name: 'component1', - annotations: { [VIEW_URL_ANNOTATION]: 'https://other.place' }, + annotations: { [ANNOTATION_VIEW_URL]: 'https://other.place' }, }, }; diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx index f16a17bc1d..eb920aaa20 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx @@ -43,7 +43,12 @@ describe('ComponentContextMenu', () => { it('should call onUnregisterEntity on button click', async () => { const mockCallback = jest.fn(); - await render(); + await render( + {}} + />, + ); const button = await screen.findByTestId('menu-button'); expect(button).toBeInTheDocument(); @@ -56,6 +61,27 @@ describe('ComponentContextMenu', () => { expect(mockCallback).toBeCalled(); }); + it('should call onInspectEntity on button click', async () => { + const mockCallback = jest.fn(); + + await render( + {}} + onInspectEntity={mockCallback} + />, + ); + + const button = await screen.findByTestId('menu-button'); + expect(button).toBeInTheDocument(); + fireEvent.click(button); + + const unregister = await screen.findByText('Inspect entity'); + expect(unregister).toBeInTheDocument(); + fireEvent.click(unregister); + + expect(mockCallback).toBeCalled(); + }); + it('supports extra items', async () => { const extra = { title: 'HELLO', @@ -66,6 +92,7 @@ describe('ComponentContextMenu', () => { await render( , ); diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index 6648514382..7bda1f4629 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -24,7 +24,8 @@ import { Popover, } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; -import Cancel from '@material-ui/icons/Cancel'; +import CancelIcon from '@material-ui/icons/Cancel'; +import BugReportIcon from '@material-ui/icons/BugReport'; import MoreVert from '@material-ui/icons/MoreVert'; import React, { useState } from 'react'; import { IconComponent } from '@backstage/core-plugin-api'; @@ -55,12 +56,14 @@ type Props = { UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[]; UNSTABLE_contextMenuOptions?: contextMenuOptions; onUnregisterEntity: () => void; + onInspectEntity: () => void; }; export const EntityContextMenu = ({ UNSTABLE_extraContextMenuItems, UNSTABLE_contextMenuOptions, onUnregisterEntity, + onInspectEntity, }: Props) => { const [anchorEl, setAnchorEl] = useState(); const classes = useStyles(); @@ -128,10 +131,21 @@ export const EntityContextMenu = ({ disabled={disableUnregister} > - + + { + onClose(); + onInspectEntity(); + }} + > + + + + + diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index c5679355e5..144e5d01a8 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -39,13 +39,14 @@ import { EntityRefLinks, FavoriteEntity, getEntityRelations, + InspectEntityDialog, UnregisterEntityDialog, useEntityCompoundName, } from '@backstage/plugin-catalog-react'; import { Box, TabProps } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; -import React, { useContext, useState } from 'react'; -import { useNavigate } from 'react-router'; +import React, { useContext, useEffect, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; type SubRoute = { @@ -177,6 +178,7 @@ export const EntityLayout = ({ }: EntityLayoutProps) => { const { kind, namespace, name } = useEntityCompoundName(); const { entity, loading, error } = useContext(EntityContext); + const location = useLocation(); const routes = useElementFilter( children, elements => @@ -212,13 +214,21 @@ export const EntityLayout = ({ ); const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); + const [inspectionDialogOpen, setInspectionDialogOpen] = useState(false); const navigate = useNavigate(); const cleanUpAfterRemoval = async () => { setConfirmationDialogOpen(false); + setInspectionDialogOpen(false); navigate('/'); }; - const showRemovalDialog = () => setConfirmationDialogOpen(true); + // Make sure to close the dialog if the user clicks links in it that navigate + // to another entity. + useEffect(() => { + setConfirmationDialogOpen(false); + setInspectionDialogOpen(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.pathname]); return ( @@ -233,7 +243,8 @@ export const EntityLayout = ({ setConfirmationDialogOpen(true)} + onInspectEntity={() => setInspectionDialogOpen(true)} /> )} @@ -267,6 +278,11 @@ export const EntityLayout = ({ onConfirm={cleanUpAfterRemoval} onClose={() => setConfirmationDialogOpen(false)} /> + setInspectionDialogOpen(false)} + /> ); }; diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx index f58e6b9b66..75155aa075 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx @@ -16,7 +16,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { DeleteEntityDialog } from './DeleteEntityDialog'; -import { ORIGIN_LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import { ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model'; import { CatalogApi } from '@backstage/catalog-client'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { screen, waitFor } from '@testing-library/react'; @@ -42,7 +42,7 @@ describe('DeleteEntityDialog', () => { name: 'n', namespace: 'ns', annotations: { - [ORIGIN_LOCATION_ANNOTATION]: 'url:http://example.com', + [ANNOTATION_ORIGIN_LOCATION]: 'url:http://example.com', }, }, spec: {}, diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index c371d6245e..82a980afcb 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -23,6 +23,7 @@ import { EntityRefLinks, FavoriteEntity, getEntityRelations, + InspectEntityDialog, UnregisterEntityDialog, useEntityCompoundName, } from '@backstage/plugin-catalog-react'; @@ -123,6 +124,11 @@ type EntityPageLayoutProps = { children?: React.ReactNode; }; +/** + * Old entity page, only used by the old router based hierarchies. + * + * @deprecated Please use CatalogEntityPage instead + */ export const EntityPageLayout = ({ children, UNSTABLE_extraContextMenuItems, @@ -138,14 +144,13 @@ export const EntityPageLayout = ({ ); const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); + const [inspectionDialogOpen, setInspectionDialogOpen] = useState(false); const navigate = useNavigate(); const cleanUpAfterRemoval = async () => { setConfirmationDialogOpen(false); navigate('/'); }; - const showRemovalDialog = () => setConfirmationDialogOpen(true); - return (
setConfirmationDialogOpen(true)} + onInspectEntity={() => setInspectionDialogOpen(true)} /> )} @@ -198,6 +204,11 @@ export const EntityPageLayout = ({ onConfirm={cleanUpAfterRemoval} onClose={() => setConfirmationDialogOpen(false)} /> + setInspectionDialogOpen(false)} + /> ); }; diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx index ae5a4684b1..2297493a7a 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx @@ -14,18 +14,17 @@ * limitations under the License. */ +import { AlphaEntity, stringifyEntityRef } from '@backstage/catalog-model'; +import { ApiProvider } from '@backstage/core-app-api'; import { CatalogApi, catalogApiRef, EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; - import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; import { EntityProcessingErrorsPanel } from './EntityProcessingErrorsPanel'; -import { AlphaEntity, getEntityName } from '@backstage/catalog-model'; -import { ApiProvider } from '@backstage/core-app-api'; describe('', () => { const getEntityAncestors: jest.MockedFunction< @@ -98,8 +97,8 @@ describe('', () => { }; getEntityAncestors.mockResolvedValue({ - root: getEntityName(entity), - items: [{ entity, parents: [] }], + rootEntityRef: stringifyEntityRef(entity), + items: [{ entity, parentEntityRefs: [] }], }); const { getByText, queryByText } = await renderInTestApp( @@ -199,10 +198,10 @@ describe('', () => { }, }; getEntityAncestors.mockResolvedValue({ - root: getEntityName(entity), + rootEntityRef: stringifyEntityRef(entity), items: [ - { entity, parents: [getEntityName(parent)] }, - { entity: parent, parents: [] }, + { entity, parentEntityRefs: [stringifyEntityRef(parent)] }, + { entity: parent, parentEntityRefs: [] }, ], }); const { getByText, queryByText } = await renderInTestApp( diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 57dd911b2d..abe34c29b9 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -49,7 +49,6 @@ export { EntityHasSubcomponentsCard, EntityHasSystemsCard, EntityLinksCard, - EntitySystemDiagramCard, RelatedEntitiesCard, } from './plugin'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 5a94cd2a09..2e4ebe9570 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -177,20 +177,6 @@ export const EntityDependsOnResourcesCard = catalogPlugin.provide( }), ); -/** - * @deprecated This component is replaced by EntityCatalogGraphCard which is imported from `@backstage/plugin-catalog-graph`. This component will be removed in an - * upcoming release - */ -export const EntitySystemDiagramCard = catalogPlugin.provide( - createComponentExtension({ - name: 'EntitySystemDiagramCard', - component: { - lazy: () => - import('./components/SystemDiagramCard').then(m => m.SystemDiagramCard), - }, - }), -); - export const RelatedEntitiesCard = catalogPlugin.provide( createComponentExtension({ name: 'RelatedEntitiesCard', diff --git a/plugins/cicd-statistics/.eslintrc.js b/plugins/cicd-statistics/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/cicd-statistics/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md new file mode 100644 index 0000000000..7258ea0570 --- /dev/null +++ b/plugins/cicd-statistics/CHANGELOG.md @@ -0,0 +1,12 @@ +# @backstage/plugin-cicd-statistics + +## 0.1.0 + +### Minor Changes + +- 770c195f34: Added new plugin "CI/CD Statistics" which charts pipeline build durations over time + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@0.6.14 diff --git a/plugins/cicd-statistics/README.md b/plugins/cicd-statistics/README.md new file mode 100644 index 0000000000..8d91d74794 --- /dev/null +++ b/plugins/cicd-statistics/README.md @@ -0,0 +1,13 @@ +# CI/CD Statistics Plugin + +This plugin shows charts of CI/CD pipeline durations over time. It expects to be used on the Software Catalog entity page, as it uses `useEntity` to figure out what component to get the build information for. + +## Usage + +> This plugin cannot be used as-is; it requires a custom implementation to fetch build information + +To use this plugin, you need to implement an API `CicdStatisticsApi` and bind it to the `cicdStatisticsApiRef`. This API is defined in `src/apis/types.ts` and is an interface with two functions, `getConfiguration(options)` and `fetchBuilds(options)`. This plugin will call `getConfiguration` to allow the implementation to specify defaults and settings for the UI. + +First time the UI shows, and each time the user changes filters and clicks `Update` to refresh the data, `fetchBuilds` is invoked with the filter options. The API implementation is the expected to fetch build information from somewhere, format it into a generic and rather simple type `Build` (also defined in `types.ts`). The API can optionally signal completion for a progress bar in the UI. + +When this plugin has fetched the builds, it will transpose the list of builds (and build stages) into a tree of build stages. As build pipelines sometimes change, certain stages might end or begin within the date range of the view (when _Normalize time range_ is enabled, which is the default). diff --git a/plugins/cicd-statistics/api-report.md b/plugins/cicd-statistics/api-report.md new file mode 100644 index 0000000000..3c7a52cdad --- /dev/null +++ b/plugins/cicd-statistics/api-report.md @@ -0,0 +1,222 @@ +## API Report File for "@backstage/plugin-cicd-statistics" + +> 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 { Entity } from '@backstage/catalog-model'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "AbortError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class AbortError extends Error {} + +// Warning: (ae-missing-release-tag) "Build" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface Build { + branchType: FilterBranchType; + duration: number; + id: string; + // (undocumented) + raw?: unknown; + requestedAt: Date; + stages: Array; + status: FilterStatusType; + triggeredBy?: TriggerReason; +} + +// Warning: (ae-missing-release-tag) "BuildWithRaw" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type BuildWithRaw = Build & { + raw: T; +}; + +// Warning: (ae-missing-release-tag) "ChartType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type ChartType = 'duration' | 'count'; + +// Warning: (ae-missing-release-tag) "ChartTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ChartTypes = Array; + +// Warning: (ae-missing-release-tag) "CicdConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface CicdConfiguration { + availableStatuses: ReadonlyArray; + defaults: Partial; + formatStageName: (parentNames: Array, stageName: string) => string; +} + +// Warning: (ae-missing-release-tag) "CicdDefaults" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface CicdDefaults { + chartTypes: Record; + collapsedLimit: number; + // (undocumented) + filterStatus: Array; + // (undocumented) + filterType: FilterBranchType | 'all'; + hideLimit: number; + lowercaseNames: boolean; + normalizeTimeRange: boolean; + // (undocumented) + timeFrom: Date; + // (undocumented) + timeTo: Date; +} + +// Warning: (ae-missing-release-tag) "CicdState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface CicdState { + // (undocumented) + builds: Array; +} + +// Warning: (ae-missing-release-tag) "CicdStatisticsApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface CicdStatisticsApi { + // (undocumented) + fetchBuilds(options: FetchBuildsOptions): Promise; + // (undocumented) + getConfiguration( + options: GetConfigurationOptions, + ): Promise>; +} + +// Warning: (ae-missing-release-tag) "cicdStatisticsApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const cicdStatisticsApiRef: ApiRef; + +// Warning: (ae-missing-release-tag) "cicdStatisticsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const cicdStatisticsPlugin: BackstagePlugin< + { + entityContent: RouteRef; + }, + {} +>; + +// Warning: (ae-missing-release-tag) "EntityCicdStatisticsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityCicdStatisticsContent: EntityPageCicdCharts; + +// Warning: (ae-missing-release-tag) "EntityPageCicdCharts" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function EntityPageCicdCharts(): JSX.Element; + +// Warning: (ae-missing-release-tag) "FetchBuildsOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface FetchBuildsOptions { + // (undocumented) + abortSignal: AbortSignal; + // (undocumented) + entity: Entity; + // (undocumented) + filterStatus: Array; + // (undocumented) + filterType: FilterBranchType | 'all'; + // (undocumented) + timeFrom: Date; + // (undocumented) + timeTo: Date; + // (undocumented) + updateProgress: UpdateProgress; +} + +// Warning: (ae-missing-release-tag) "FilterBranchType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type FilterBranchType = 'master' | 'branch'; + +// Warning: (ae-missing-release-tag) "FilterStatusType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type FilterStatusType = + | 'unknown' + | 'enqueued' + | 'scheduled' + | 'running' + | 'aborted' + | 'succeeded' + | 'failed' + | 'stalled' + | 'expired'; + +// Warning: (ae-missing-release-tag) "GetConfigurationOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface GetConfigurationOptions { + // (undocumented) + entity: Entity; +} + +// Warning: (ae-missing-release-tag) "Stage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface Stage { + duration: number; + // (undocumented) + name: string; + stages?: Array; + status: FilterStatusType; +} + +// Warning: (ae-missing-release-tag) "statusTypes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const statusTypes: Array; + +// Warning: (ae-missing-release-tag) "TriggerReason" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type TriggerReason = + /** Triggered by source code management, e.g. a Github hook */ + | 'scm' + /** Triggered manually */ + | 'manual' + /** Triggered internally (non-scm, or perhaps after being delayed/enqueued) */ + | 'internal' + /** Triggered for some other reason */ + | 'other'; + +// Warning: (ae-missing-release-tag) "triggerReasons" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const triggerReasons: Array; + +// Warning: (ae-missing-release-tag) "UpdateProgress" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface UpdateProgress { + // (undocumented) + (completed: number, total: number, started?: number): void; + // (undocumented) + ( + steps: Array<{ + title: string; + completed: number; + total: number; + started?: number; + }>, + ): void; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json new file mode 100644 index 0000000000..a058917b09 --- /dev/null +++ b/plugins/cicd-statistics/package.json @@ -0,0 +1,57 @@ +{ + "name": "@backstage/plugin-cicd-statistics", + "description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/cicd-statistics" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "devDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "@types/luxon": "^2.0.5" + }, + "dependencies": { + "@backstage/catalog-model": "^0.9.8", + "@backstage/core-plugin-api": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.6.14", + "@date-io/luxon": "^1.3.13", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.11.2", + "@material-ui/lab": "4.0.0-alpha.57", + "@material-ui/pickers": "^3.3.10", + "humanize-duration": "^3.27.0", + "already": "^3.2.0", + "lodash": "^4.17.21", + "luxon": "^2.0.2", + "react-use": "^17.3.1", + "recharts": "^2.1.5" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/cicd-statistics/src/apis/cicd-statistics.ts b/plugins/cicd-statistics/src/apis/cicd-statistics.ts new file mode 100644 index 0000000000..714900a1bd --- /dev/null +++ b/plugins/cicd-statistics/src/apis/cicd-statistics.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core-plugin-api'; + +import { CicdStatisticsApi } from './types'; + +export const cicdStatisticsApiRef = createApiRef({ + id: 'cicd-statistics-api', +}); diff --git a/plugins/cicd-statistics/src/apis/index.ts b/plugins/cicd-statistics/src/apis/index.ts new file mode 100644 index 0000000000..cb2fff3700 --- /dev/null +++ b/plugins/cicd-statistics/src/apis/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; +export * from './cicd-statistics'; diff --git a/plugins/cicd-statistics/src/apis/types.ts b/plugins/cicd-statistics/src/apis/types.ts new file mode 100644 index 0000000000..0a87b40fc2 --- /dev/null +++ b/plugins/cicd-statistics/src/apis/types.ts @@ -0,0 +1,275 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; + +/** + * This is a generic enum of build statuses. + * + * If all of these aren't applicable to the underlying CI/CD, these can be + * configured to be hidden, using the `availableStatuses` in `CicdConfiguration`. + */ +export type FilterStatusType = + | 'unknown' + | 'enqueued' + | 'scheduled' + | 'running' + | 'aborted' + | 'succeeded' + | 'failed' + | 'stalled' + | 'expired'; +export const statusTypes: Array = [ + 'succeeded', + 'failed', + 'enqueued', + 'scheduled', + 'running', + 'aborted', + 'stalled', + 'expired', + 'unknown', +]; + +/** + * The branch enum of either 'master' or 'branch' (or possibly the meta 'all'). + * + * The concept of what constitues a master branch is generic. It might be called + * something like 'release' or 'main' or 'trunk' in the underlying CI/CD system, + * which is then up to the Api to map accordingly. + */ +export type FilterBranchType = 'master' | 'branch'; + +export type TriggerReason = + /** Triggered by source code management, e.g. a Github hook */ + | 'scm' + /** Triggered manually */ + | 'manual' + /** Triggered internally (non-scm, or perhaps after being delayed/enqueued) */ + | 'internal' + /** Triggered for some other reason */ + | 'other'; + +export const triggerReasons: Array = [ + 'scm', + 'manual', + 'internal', + 'other', +]; + +/** + * A Stage is a part of either a Build or a parent Stage. + * + * This may be called things like Stage or Step or Task in CI/CD systems, but is + * generic here. There's also no concept of parallelism which might exist within + * some stages. + */ +export interface Stage { + name: string; + + /** The status of the stage */ + status: FilterStatusType; + + /** Stage duration in milliseconds */ + duration: number; + + /** Sub stages within this stage */ + stages?: Array; +} + +/** + * Generic Build type. + * + * A build has e.g. a build type (master/branch), a status and (possibly) sub stages. + */ +export interface Build { + raw?: unknown; + + /** Build id */ + id: string; + + /** The reason this build was started */ + triggeredBy?: TriggerReason; + + /** The status of the build */ + status: FilterStatusType; + + /** Branch type */ + branchType: FilterBranchType; + + /** Time when the build started */ + requestedAt: Date; + + /** The overall duration of the build */ + duration: number; + + /** Top-level build stages */ + stages: Array; +} + +/** + * Helper type which is a Build with a certain typed 'raw' field. + * + * This can be useful in an Api to use while mapping internal data structures + * (raw) into generic builds. + */ +export type BuildWithRaw = Build & { + raw: T; +}; + +/** + * Chart type. + * + * Values are: + * * `duration`: shows an area chart of the duration over time + * * `count`: shows a bar chart of the number of build per day + */ +export type ChartType = 'duration' | 'count'; +export type ChartTypes = Array; + +/** + * Default settings for the fetching options and view options. + * + * These are all optional, but can be overridden from the Api to whatever makes + * most sense for that implementation. + */ +export interface CicdDefaults { + timeFrom: Date; + timeTo: Date; + filterStatus: Array; + filterType: FilterBranchType | 'all'; + + /** Lower-case all stage names (to potentially merge stages with different cases) */ + lowercaseNames: boolean; + /** Normalize the from-to date range in all charts */ + normalizeTimeRange: boolean; + /** Default collapse the stages with a max-duration below this value */ + collapsedLimit: number; + /** Default hide stages with a max-duration below this value */ + hideLimit: number; + /** Chart types per status */ + chartTypes: Record; +} + +/** + * A configuration interface which the Api must implement. + * + * When the UI for the CI/CD Statistics is loaded, it begins with fetching the + * configuration before anything else. + * + * All of these fields are optional though, and will fallback to hard-coded defaults. + */ +export interface CicdConfiguration { + /** + * This field can be used to override what statuses are available + */ + availableStatuses: ReadonlyArray; + + /** + * When transposing the list of builds into a tree of stages, the stage names + * will be transformed through this function. + * + * Override this for a custom implementation. The default will try to remove + * parent names off of child names, if they are prepended by them. + * + * For example; if a stage has the name 'Install' and a child stage has the + * name 'Install - Fetch dependencies', the child name will be replaced with + * 'Fetch dependencies'. + */ + formatStageName: (parentNames: Array, stageName: string) => string; + + /** + * Default options for the UI + */ + defaults: Partial; +} + +/** + * If the Api implements support for aborting the fetching of builds, throw an + * AbortError of this type (or any other error with name === 'AbortError'). + */ +export class AbortError extends Error {} + +/** + * The result type for `fetchBuilds`. + */ +export interface CicdState { + builds: Array; +} + +/** + * When fetching, if applicable, the Api can feedback progress back to the UI. + * + * Use the `updateProgress(completed, total, started?)` to signal that + * `completed` builds out of a `total` has finished. Optionally use the + * `started` to signal how many builds have been started in total (i.e. at least + * the amount of `completed`). + * + * This can be called at any rate. Rate limiting (debouncing) is implemented in + * the UI. + * + * Optionally this can signal multiple progresses in several steps + */ +export interface UpdateProgress { + (completed: number, total: number, started?: number): void; + ( + steps: Array<{ + title: string; + completed: number; + total: number; + started?: number; + }>, + ): void; +} + +/** + * When reading configuration, the Api can return a custom settings depending on + * the entity being viewed. + */ +export interface GetConfigurationOptions { + entity: Entity; +} + +/** + * When fetching, the Api should fetch build information about the `entity` and + * respect the `timeFrom`, `timeTo`, `filterStatus` and `filterType`. + * + * Optionally implement support for `updateProgress` and `abortSignal` if + * preferred. + * + * When the UI re-fetches, it will abort any previous fetching, so polling + * `abortSignal.aborted`, and possibly throwing an `AbortError`, can be useful. + */ +export interface FetchBuildsOptions { + entity: Entity; + updateProgress: UpdateProgress; + abortSignal: AbortSignal; + timeFrom: Date; + timeTo: Date; + filterStatus: Array; + filterType: FilterBranchType | 'all'; +} + +/** + * The interface which is mapped to the `cicdStatisticsApiRef` which is used by + * the UI. + */ +export interface CicdStatisticsApi { + getConfiguration( + options: GetConfigurationOptions, + ): Promise>; + fetchBuilds(options: FetchBuildsOptions): Promise; +} diff --git a/plugins/cicd-statistics/src/charts/colors.ts b/plugins/cicd-statistics/src/charts/colors.ts new file mode 100644 index 0000000000..670b214665 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/colors.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FilterStatusType, TriggerReason } from '../apis/types'; + +export const statusColorMap: Record = { + unknown: '#3d01a4', + enqueued: '#7ad1b9', + scheduled: '#0391ce', + running: '#f3f318', + aborted: '#8600af', + succeeded: '#66b032', + failed: '#fe2712', + stalled: '#fb9904', + expired: '#a7194b', +}; + +export const triggerColorMap: Record = { + scm: '#0391ce', + manual: '#a7194b', + internal: '#82ca9d', + other: '#f3f318', +}; + +export const fireColors: Array<[percent: string, color: string]> = [ + ['5%', '#e19678'], + ['30%', '#dfe178'], + ['50%', '#82ca9d'], + ['95%', '#82ca9d'], +]; + +export const colorStroke = '#c0c0c0'; +export const colorStrokeAvg = '#788ee1'; diff --git a/plugins/cicd-statistics/src/charts/logic/analysis.ts b/plugins/cicd-statistics/src/charts/logic/analysis.ts new file mode 100644 index 0000000000..4333ae3a6f --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/analysis.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FilterStatusType } from '../../apis/types'; +import { ChartableStageAnalysis, ChartableStageDatapoints } from '../types'; + +export function getAnalysis( + values: Array, + status: FilterStatusType, +): ChartableStageAnalysis { + const analysis: ChartableStageAnalysis = { + max: 0, + min: 0, + avg: 0, + med: 0, + }; + + const definedValues = values + .filter(value => typeof value[status] !== 'undefined') + .map(value => value[status]!) + .sort((a, b) => a - b); + + analysis.max = definedValues[definedValues.length - 1] ?? 0; + + analysis.min = definedValues[0] ?? 0; + + analysis.avg = + definedValues.length === 0 + ? 0 + : definedValues.reduce((prev, cur) => prev + cur, 0) / values.length; + + analysis.med = definedValues[Math.ceil(definedValues.length / 2)] ?? 0; + + return analysis; +} + +export function makeCombinedAnalysis( + analysis: Record, + allDurations: Array, +): ChartableStageAnalysis { + if (analysis.succeeded) { + // If succeeded is a viewed status, it's probably what's expected to see + // overall. Otherwise combine all other. + return analysis.succeeded; + } + + const analysisValues = Object.values(analysis); + + const max = analysisValues.reduce((prev, cur) => Math.max(prev, cur.max), 0); + const min = analysisValues.reduce( + (prev, cur) => Math.min(prev, cur.min), + max, + ); + const avg = !allDurations.length + ? 0 + : allDurations.reduce((prev, cur) => prev + cur, 0) / allDurations.length; + allDurations.sort((a, b) => a - b); + const med = allDurations[Math.ceil(allDurations.length / 2)] ?? 0; + + return { + max, + min, + avg, + med, + }; +} diff --git a/plugins/cicd-statistics/src/charts/logic/conversions.ts b/plugins/cicd-statistics/src/charts/logic/conversions.ts new file mode 100644 index 0000000000..c548b0e2f5 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/conversions.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { map } from 'already'; + +import { Build, Stage, FilterStatusType } from '../../apis/types'; +import { ChartableStage, ChartableStagesAnalysis } from '../types'; +import { getOrSetStage, makeStage, sortStatuses } from './utils'; +import { finalizeStage } from './finalize-stage'; +import { dailySummary } from './daily-summary'; + +export interface ChartableStagesOptions { + normalizeTimeRange: boolean; +} + +/** + * Converts a list of builds, each with a tree of stages (and durations) into a + * merged tree of stages, and calculates {avg, min, max} of each stage. + */ +export async function buildsToChartableStages( + builds: Array, + options: ChartableStagesOptions, +): Promise { + const { normalizeTimeRange } = options; + + const total: ChartableStage = makeStage('Total'); + + const recurseDown = ( + stageMap: Map, + stage: Stage, + __epoch: number, + ) => { + const { name, status, duration } = stage; + + const subChartableStage = getOrSetStage(stageMap, name); + + subChartableStage.statusSet.add(status); + subChartableStage.values.push({ + __epoch, + [status]: duration, + [`${status} avg`]: duration, + }); + + stage.stages?.forEach(subStage => { + recurseDown(subChartableStage.stages, subStage, __epoch); + }); + }; + + const stages = new Map(); + + await map(builds, { chunk: 'idle' }, build => { + const { duration, requestedAt, status } = build; + const __epoch = requestedAt.getTime(); + + total.statusSet.add(status); + total.values.push({ + __epoch, + [status]: duration, + [`${status} avg`]: duration, + }); + + build.stages?.forEach(subStage => { + recurseDown(stages, subStage, __epoch); + }); + }); + + const allEpochs = normalizeTimeRange + ? builds.map(build => build.requestedAt.getTime()) + : []; + + // Recurse down again and calculate averages + await map([...stages.values()], { chunk: 'idle' }, stage => + finalizeStage(stage, { allEpochs, averageWidth: 10 }), + ); + finalizeStage(total, { allEpochs, averageWidth: 10 }); + + const daily = dailySummary(builds); + + const statuses = findStatuses(total, [...stages.values()]); + + return { daily, total, stages, statuses }; +} + +function findStatuses( + total: ChartableStage, + stages: Array, +): Array { + const statuses = new Set(); + + const addStatuses = (set: Set) => { + set.forEach(status => { + statuses.add(status); + }); + }; + + addStatuses(total.statusSet); + + const recurse = (subStages: Array) => { + subStages.forEach(stage => { + addStatuses(stage.statusSet); + recurse([...stage.stages.values()]); + }); + }; + recurse(stages); + + return sortStatuses([...statuses]); +} diff --git a/plugins/cicd-statistics/src/charts/logic/count-builds-per-day.ts b/plugins/cicd-statistics/src/charts/logic/count-builds-per-day.ts new file mode 100644 index 0000000000..54e47c3f5b --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/count-builds-per-day.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { groupBy } from 'lodash'; + +import { FilterStatusType, statusTypes } from '../../apis/types'; +import { Countify, ChartableStageDatapoints } from '../types'; +import { startOfDay } from './utils'; + +export function countBuildsPerDay( + values: ReadonlyArray, +) { + const days = groupBy(values, value => startOfDay(value.__epoch)); + Object.entries(days).forEach(([_startOfDay, valuesThisDay]) => { + const counts = Object.fromEntries( + statusTypes + .map( + type => + [ + type, + valuesThisDay.filter(value => value[type] !== undefined).length, + ] as const, + ) + .filter(([_type, count]) => count > 0) + .map(([type, count]): [Countify, number] => [ + `${type} count`, + count, + ]), + ); + + // Assign the count for this day to the first value this day + Object.assign(valuesThisDay[0], counts); + }); +} diff --git a/plugins/cicd-statistics/src/charts/logic/daily-summary.ts b/plugins/cicd-statistics/src/charts/logic/daily-summary.ts new file mode 100644 index 0000000000..93c7b07c34 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/daily-summary.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { groupBy, countBy } from 'lodash'; + +import { Build } from '../../apis/types'; +import { + Epoch, + TriggerReasonsDatapoint, + StatusesDatapoint, + ChartableDaily, +} from '../types'; +import { sortStatuses, sortTriggerReasons, startOfDay } from './utils'; + +export function dailySummary(builds: ReadonlyArray): ChartableDaily { + const triggersDaily = countTriggersPerDay(builds); + const statusesDaily = countStatusesPerDay(builds); + + const { triggerReasons } = triggersDaily; + const { statuses } = statusesDaily; + + const reasonMap = new Map( + triggersDaily.values.map(value => [value.__epoch, value]), + ); + const statusMap = new Map( + statusesDaily.values.map(value => [value.__epoch, value]), + ); + + const days = Object.keys( + groupBy(builds, value => startOfDay(value.requestedAt)), + ) + .map(epoch => parseInt(epoch, 10)) + .sort(); + + return { + values: days.map(epoch => ({ + __epoch: epoch, + ...reasonMap.get(epoch), + ...statusMap.get(epoch), + })), + triggerReasons, + statuses, + }; +} + +function countTriggersPerDay(builds: ReadonlyArray) { + const days = groupBy(builds, value => startOfDay(value.requestedAt)); + + const triggerReasons = sortTriggerReasons([ + ...new Set( + builds + .map(({ triggeredBy }) => triggeredBy) + .filter((v): v is NonNullable => !!v), + ), + ]); + + const values = Object.entries(days).map(([epoch, buildsThisDay]) => { + const datapoint = Object.fromEntries( + triggerReasons + .map(reason => [ + reason, + buildsThisDay.filter(build => build.triggeredBy === reason).length, + ]) + .filter(([_type, count]) => count > 0), + ) as Omit; + + // Assign the count for this day to the first value this day + const value: Epoch & TriggerReasonsDatapoint = Object.assign(datapoint, { + __epoch: parseInt(epoch, 10), + }); + + return value; + }); + + return { triggerReasons, values }; +} + +function countStatusesPerDay(builds: ReadonlyArray) { + const days = groupBy(builds, value => startOfDay(value.requestedAt)); + + const foundStatuses = new Set(); + + const values = Object.entries(days).map(([epoch, buildsThisDay]) => { + const byStatus = countBy(buildsThisDay, 'status'); + + const value: Epoch & StatusesDatapoint = { + __epoch: parseInt(epoch, 10), + ...byStatus, + }; + + Object.keys(byStatus).forEach(status => { + foundStatuses.add(status); + }); + + return value; + }); + + return { + statuses: sortStatuses([...foundStatuses]), + values, + }; +} diff --git a/plugins/cicd-statistics/src/charts/logic/finalize-stage.ts b/plugins/cicd-statistics/src/charts/logic/finalize-stage.ts new file mode 100644 index 0000000000..bb54fbf41e --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/finalize-stage.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FilterStatusType, statusTypes } from '../../apis/types'; +import { Averagify, ChartableStage } from '../types'; +import { countBuildsPerDay } from './count-builds-per-day'; +import { getAnalysis, makeCombinedAnalysis } from './analysis'; +import { average } from './utils'; + +interface FinalizeStageOptions { + averageWidth: number; + allEpochs: Array; +} + +/** + * Calculate: + * * {avg, min, max} + * * count per day + * of a stage and its sub stages, recursively. + * + * This is calculated per status (successful, failed, etc). + */ +export function finalizeStage( + stage: ChartableStage, + options: FinalizeStageOptions, +) { + const { averageWidth, allEpochs } = options; + const { values, analysis, combinedAnalysis } = stage; + + if (allEpochs.length > 0) { + const valueEpochs = new Set(values.map(value => value.__epoch)); + + allEpochs.forEach(epoch => { + if (!valueEpochs.has(epoch)) { + values.push({ __epoch: epoch }); + } + }); + } + + values.sort((a, b) => a.__epoch - b.__epoch); + + countBuildsPerDay(values); + + const allDurations: Array = []; + + statusTypes.forEach(status => { + analysis[status] = getAnalysis(values, status); + + const durationsIndexes = values + .map(value => value[status]) + .map((duration, index) => ({ index, duration })) + .filter(({ duration }) => typeof duration !== 'undefined') + .map(({ index }) => index); + const durationsDense = values + .map(value => value[status]) + .filter( + (duration): duration is number => typeof duration !== 'undefined', + ); + + durationsDense.forEach(dur => allDurations.push(dur)); + + const averages = durationsDense.map((_, i) => + average( + durationsDense.slice( + Math.max(i - averageWidth, 0), + Math.min(i + averageWidth, durationsDense.length), + ), + ), + ); + + averages.forEach((avg, index) => { + const key: Averagify = `${status} avg`; + values[durationsIndexes[index]][key] = avg; + }); + }); + + Object.assign(combinedAnalysis, makeCombinedAnalysis(analysis, allDurations)); + + stage.stages.forEach(subStage => finalizeStage(subStage, options)); +} diff --git a/plugins/cicd-statistics/src/charts/logic/utils.test.ts b/plugins/cicd-statistics/src/charts/logic/utils.test.ts new file mode 100644 index 0000000000..72e0a22101 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/utils.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { sortTriggerReasons } from './utils'; + +describe('daily-summary', () => { + it('sortTriggerReasons', () => { + const values = ['a', 'manual', 'b', 'other', 'c', 'scm', 'd']; + const expected = ['manual', 'scm', 'other', 'a', 'b', 'c', 'd']; + + expect(sortTriggerReasons(values)).toStrictEqual(expected); + expect(sortTriggerReasons(values.reverse())).toStrictEqual(expected); + }); +}); diff --git a/plugins/cicd-statistics/src/charts/logic/utils.ts b/plugins/cicd-statistics/src/charts/logic/utils.ts new file mode 100644 index 0000000000..f3a0087a3d --- /dev/null +++ b/plugins/cicd-statistics/src/charts/logic/utils.ts @@ -0,0 +1,87 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DateTime } from 'luxon'; + +import { FilterStatusType, statusTypes } from '../../apis/types'; +import { ChartableStage } from '../types'; + +export function average(values: number[]): number { + return !values.length + ? 0 + : Math.round(values.reduce((prev, cur) => prev + cur, 0) / values.length); +} + +export function getOrSetStage( + stages: Map, + name: string, +): ChartableStage { + const stage = stages.get(name); + if (stage) return stage; + + const newStage: ChartableStage = makeStage(name); + stages.set(name, newStage); + return newStage; +} + +export function makeStage(name: string): ChartableStage { + return { + analysis: { + unknown: { avg: 0, med: 0, max: 0, min: 0 }, + enqueued: { avg: 0, med: 0, max: 0, min: 0 }, + scheduled: { avg: 0, med: 0, max: 0, min: 0 }, + running: { avg: 0, med: 0, max: 0, min: 0 }, + aborted: { avg: 0, med: 0, max: 0, min: 0 }, + succeeded: { avg: 0, med: 0, max: 0, min: 0 }, + failed: { avg: 0, med: 0, max: 0, min: 0 }, + stalled: { avg: 0, med: 0, max: 0, min: 0 }, + expired: { avg: 0, med: 0, max: 0, min: 0 }, + }, + combinedAnalysis: { avg: 0, med: 0, max: 0, min: 0 }, + statusSet: new Set(), + name, + values: [], + stages: new Map(), + }; +} + +export function startOfDay(date: number | Date) { + if (typeof date === 'number') { + return DateTime.fromMillis(date).startOf('day').toMillis(); + } + return DateTime.fromJSDate(date).startOf('day').toMillis(); +} + +export function sortTriggerReasons(reasons: Array): Array { + return reasons.sort((a, b) => { + if (a === 'manual') return -1; + else if (b === 'manual') return 1; + else if (a === 'scm') return -1; + else if (b === 'scm') return 1; + else if (a === 'other') return -1; + else if (b === 'other') return 1; + return a.localeCompare(b); + }); +} + +export function sortStatuses(statuses: Array): Array { + return [ + ...statusTypes.filter(status => statuses.includes(status)), + ...statuses + .filter(status => !(statusTypes as Array).includes(status)) + .sort((a, b) => a.localeCompare(b)), + ]; +} diff --git a/plugins/cicd-statistics/src/charts/stage-chart.tsx b/plugins/cicd-statistics/src/charts/stage-chart.tsx new file mode 100644 index 0000000000..ba3474153c --- /dev/null +++ b/plugins/cicd-statistics/src/charts/stage-chart.tsx @@ -0,0 +1,269 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { CSSProperties, Fragment, useMemo } from 'react'; +import { + Area, + Bar, + ComposedChart, + XAxis, + YAxis, + YAxisProps, + CartesianGrid, + Legend, + LegendProps, + Line, + Tooltip, + ResponsiveContainer, +} from 'recharts'; +import Alert from '@material-ui/lab/Alert'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Grid, + Typography, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { capitalize } from 'lodash'; + +import { useZoom, useZoomArea } from './zoom'; +import { CicdDefaults, statusTypes } from '../apis/types'; +import { ChartableStage } from './types'; +import { + pickElements, + labelFormatter, + tickFormatterX, + tickFormatterY, + tooltipValueFormatter, + formatDuration, +} from '../components/utils'; +import { + statusColorMap, + fireColors, + colorStroke, + colorStrokeAvg, +} from './colors'; + +const fullWidth: CSSProperties = { width: '100%' }; +const noUserSelect: CSSProperties = { userSelect: 'none' }; + +const transitionProps = { unmountOnExit: true }; + +export interface StageChartProps { + stage: ChartableStage; + + chartTypes: CicdDefaults['chartTypes']; + defaultCollapsed?: number; + defaultHidden?: number; + zeroYAxis?: boolean; +} + +export function StageChart(props: StageChartProps) { + const { stage, ...chartOptions } = props; + const { + chartTypes, + defaultCollapsed = 0, + defaultHidden = 0, + zeroYAxis = false, + } = chartOptions; + + const { zoomFilterValues } = useZoom(); + const { zoomProps, getZoomArea } = useZoomArea(); + + const ticks = useMemo( + () => pickElements(stage.values, 8).map(val => val.__epoch), + [stage.values], + ); + const domainY = useMemo( + () => [zeroYAxis ? 0 : 'auto', 'auto'] as YAxisProps['domain'], + [zeroYAxis], + ); + const statuses = useMemo( + () => statusTypes.filter(status => stage.statusSet.has(status)), + [stage.statusSet], + ); + const legendPayload = useMemo( + (): LegendProps['payload'] => + statuses.map(status => ({ + value: capitalize(status), + type: 'line', + id: status, + color: statusColorMap[status], + })), + [statuses], + ); + + const subStages = useMemo( + () => + new Map( + [...stage.stages.entries()].filter( + ([_name, subStage]) => subStage.combinedAnalysis.max > defaultHidden, + ), + ), + [stage.stages, defaultHidden], + ); + + const zoomFilteredValues = useMemo( + () => zoomFilterValues(stage.values), + [stage.values, zoomFilterValues], + ); + + return stage.combinedAnalysis.max < defaultHidden ? null : ( + defaultCollapsed} + TransitionProps={transitionProps} + > + }> + + {stage.name} (med {formatDuration(stage.combinedAnalysis.med)}, avg{' '} + {formatDuration(stage.combinedAnalysis.avg)}) + + + + {stage.values.length === 0 ? ( + No data + ) : ( + + + + + + + {fireColors.map(([percent, color]) => ( + + ))} + + + {statuses.length > 1 && } + + + + + + {statuses.reverse().map(status => ( + + {!chartTypes[status].includes('duration') ? null : ( + <> + 1 + ? statusColorMap[status] + : colorStroke + } + fillOpacity={statuses.length > 1 ? 0.5 : 1} + fill={ + statuses.length > 1 + ? statusColorMap[status] + : 'url(#colorDur)' + } + connectNulls + /> + 1 + ? statusColorMap[status] + : colorStrokeAvg + } + opacity={0.8} + strokeWidth={2} + dot={false} + connectNulls + /> + + )} + {!chartTypes[status].includes('count') ? null : ( + + )} + + ))} + {getZoomArea({ yAxisId: 1 })} + + + + {subStages.size === 0 ? null : ( + + + }> + Sub stages ({subStages.size}) + + +
+ {[...subStages.values()].map(subStage => ( + + ))} +
+
+
+
+ )} +
+ )} +
+
+ ); +} diff --git a/plugins/cicd-statistics/src/charts/status-chart.tsx b/plugins/cicd-statistics/src/charts/status-chart.tsx new file mode 100644 index 0000000000..7b07885ca5 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/status-chart.tsx @@ -0,0 +1,220 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Fragment, useMemo } from 'react'; +import { + Area, + Bar, + ComposedChart, + XAxis, + YAxis, + CartesianGrid, + Legend, + LegendProps, + Tooltip, + ResponsiveContainer, +} from 'recharts'; +import Alert from '@material-ui/lab/Alert'; +import { + Accordion, + AccordionSummary, + AccordionDetails, + Typography, +} from '@material-ui/core'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { capitalize } from 'lodash'; + +import { useZoom, useZoomArea } from './zoom'; +import { FilterStatusType, TriggerReason } from '../apis/types'; +import { labelFormatterWithoutTime, tickFormatterX } from '../components/utils'; +import { statusColorMap, triggerColorMap } from './colors'; +import { ChartableStagesAnalysis } from './types'; + +export interface StatusChartProps { + analysis: ChartableStagesAnalysis; +} + +export function StatusChart(props: StatusChartProps) { + const { analysis } = props; + + const { zoomFilterValues } = useZoom(); + const { zoomProps, getZoomArea } = useZoomArea(); + + const values = useMemo(() => { + return analysis.daily.values.map(value => { + const totTriggers = analysis.daily.triggerReasons.reduce( + (prev, cur) => prev + (value[cur as TriggerReason] ?? 0), + 0, + ); + + if (!totTriggers) { + return value; + } + + return { + ...value, + ...Object.fromEntries( + analysis.daily.triggerReasons.map(reason => [ + reason, + (value[reason as TriggerReason] ?? 0) / totTriggers, + ]), + ), + }; + }); + }, [analysis.daily]); + + const triggerReasonLegendPayload = useMemo( + (): NonNullable => + analysis.daily.triggerReasons.map(reason => ({ + value: humanTriggerReason(reason), + type: 'line', + id: reason, + color: triggerColorMap[reason as TriggerReason] ?? '', + })), + [analysis.daily.triggerReasons], + ); + + const statusesLegendPayload = useMemo( + (): NonNullable => + analysis.daily.statuses.map(status => ({ + value: capitalize(status), + type: 'line', + id: status, + color: statusColorMap[status as FilterStatusType] ?? '', + })), + [analysis.daily.statuses], + ); + + const legendPayload = useMemo( + (): NonNullable => [ + ...triggerReasonLegendPayload, + ...statusesLegendPayload, + ], + [statusesLegendPayload, triggerReasonLegendPayload], + ); + + const tooltipFormatter = useMemo(() => { + const reasonSet = new Set(analysis.daily.triggerReasons); + + return (percentOrCount: number, name: string) => { + const label = reasonSet.has(name) + ? humanTriggerReason(name) + : capitalize(name); + const valueText = reasonSet.has(name) + ? `${(percentOrCount * 100).toFixed(0)}%` + : percentOrCount; + + return [ + + {label}: {valueText} + , + null, + ]; + }; + }, [analysis.daily.triggerReasons]); + + const zoomFilteredValues = useMemo( + () => zoomFilterValues(values), + [values, zoomFilterValues], + ); + + const barSize = getBarSize(analysis.daily.values.length); + + return ( + 1}> + }> + + Build count per status over build trigger reason + + + + {values.length === 0 ? ( + No data + ) : ( + + + + + + + + + {triggerReasonLegendPayload.map(reason => ( + + + + ))} + {[...analysis.daily.statuses].reverse().map(status => ( + + + + ))} + {getZoomArea({ yAxisId: 1 })} + + + )} + + + ); +} + +function humanTriggerReason(reason: string): string { + if ((reason as TriggerReason) === 'manual') { + return 'Triggered manually'; + } else if ((reason as TriggerReason) === 'scm') { + return 'Triggered by SCM'; + } else if ((reason as TriggerReason) === 'internal') { + return 'Triggered internally'; + } else if ((reason as TriggerReason) === 'other') { + return 'Triggered by another reason'; + } + return `Triggered by ${reason}`; +} + +function getBarSize(count: number): number { + if (count < 20) { + return 10; + } else if (count < 40) { + return 8; + } + return 5; +} diff --git a/plugins/cicd-statistics/src/charts/types.ts b/plugins/cicd-statistics/src/charts/types.ts new file mode 100644 index 0000000000..88c0085f77 --- /dev/null +++ b/plugins/cicd-statistics/src/charts/types.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FilterStatusType, TriggerReason } from '../apis/types'; + +export type Averagify = `${T} avg`; +export type Countify = `${T} count`; + +export type Epoch = { __epoch: number }; + +export type ChartableStageDatapoints = Epoch & { + [status in FilterStatusType]?: number; +} & { + [status in Averagify]?: number; +} & { + [status in Countify]?: number; +}; + +export interface ChartableStageAnalysis { + /** Maximum duration */ + max: number; + /** Minimum duration */ + min: number; + /** Average duration */ + avg: number; + /** Median duration */ + med: number; +} + +export interface ChartableStage { + analysis: Record; + combinedAnalysis: ChartableStageAnalysis; + name: string; + values: Array; + statusSet: Set; + + stages: Map; +} + +export type TriggerReasonsDatapoint = { + [K in TriggerReason]?: number; +}; +export type StatusesDatapoint = { [status in FilterStatusType]?: number }; + +export type ChartableDailyDatapoint = Epoch & + TriggerReasonsDatapoint & + StatusesDatapoint; + +export interface ChartableDaily { + values: Array; + + /** + * The build trigger reasons + */ + triggerReasons: Array; + + /** + * The top-level (build) statuses + */ + statuses: Array; +} + +export interface ChartableStagesAnalysis { + /** + * Summary of statuses and trigger reasons per day + */ + daily: ChartableDaily; + + /** + * Total aggregates of sub stages + */ + total: ChartableStage; + + /** + * Top-level stages {name -> stage} + */ + stages: Map; + + /** + * All statuses found deeper in the stage tree. A stage might have been + * _aborted_ although the build actually _failed_, e.g. + */ + statuses: Array; +} diff --git a/plugins/cicd-statistics/src/charts/zoom.tsx b/plugins/cicd-statistics/src/charts/zoom.tsx new file mode 100644 index 0000000000..57e2b8678f --- /dev/null +++ b/plugins/cicd-statistics/src/charts/zoom.tsx @@ -0,0 +1,221 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { throttle } from 'lodash'; +import React, { + PropsWithChildren, + Dispatch, + SetStateAction, + Fragment, + useContext, + useState, + useCallback, + useMemo, + useEffect, +} from 'react'; +import { ReferenceArea } from 'recharts'; + +import type { Epoch } from './types'; + +interface ZoomState { + left?: number; + right?: number; +} + +interface ZoomContext { + registerSelection(setter: Dispatch): void; + setSelectState: Dispatch>; + + zoomState: ZoomState; + setZoomState: Dispatch>; + + resetZoom: () => void; +} + +const context = React.createContext(undefined as any); + +export function ZoomProvider({ children }: PropsWithChildren<{}>) { + const [registeredSelectors, setRegisteredSelectors] = useState< + Array> + >([]); + const [selectState, setSelectState] = useState({}); + const [zoomState, setZoomState] = useState({}); + + const registerSelection = useCallback( + (selector: Dispatch) => { + setRegisteredSelectors(old => [...old, selector]); + + return () => { + setRegisteredSelectors(old => old.filter(sel => sel === selector)); + }; + }, + [setRegisteredSelectors], + ); + + const callSelectors = useCallback( + (state: ZoomState) => { + registeredSelectors.forEach(selector => { + selector(state); + }); + }, + [registeredSelectors], + ); + + const throttledCallSelectors = useMemo( + () => throttle(callSelectors, 200), + [callSelectors], + ); + + useEffect(() => { + throttledCallSelectors({ + left: selectState.left, + right: selectState.right, + }); + }, [selectState.left, selectState.right, throttledCallSelectors]); + + const resetZoom = useCallback(() => { + setSelectState({}); + setZoomState({}); + }, [setSelectState, setZoomState]); + + const value = useMemo( + (): ZoomContext => ({ + registerSelection, + setSelectState, + + zoomState, + setZoomState, + + resetZoom, + }), + [registerSelection, setSelectState, zoomState, setZoomState, resetZoom], + ); + + return ; +} + +export function useZoom() { + const { zoomState, resetZoom } = useContext(context); + + const zoomFilterValues = useCallback( + (values: Array): Array => { + const { left, right } = zoomState; + return left === undefined || right === undefined + ? values + : values.filter(({ __epoch }) => __epoch > left && __epoch < right); + }, + [zoomState], + ); + + return useMemo( + () => ({ + resetZoom, + zoomState, + zoomFilterValues, + }), + [resetZoom, zoomState, zoomFilterValues], + ); +} + +export interface ZoomAreaProps { + yAxisId?: number | string | undefined; +} + +export function useZoomArea() { + const [showSelection, setShowSelection] = useState(false); + const [state, setState] = useState({}); + const { setSelectState, setZoomState, registerSelection } = + useContext(context); + + const onMouseDown = useCallback( + (e: any) => { + if (!e?.activeLabel) return; + + setSelectState({ left: e.activeLabel }); + setShowSelection(true); + }, + [setSelectState, setShowSelection], + ); + + const onMouseMove = useCallback( + (e: any) => { + if (!e?.activeLabel) return; + + setSelectState(area => { + if (!area.left) { + return area; + } + return { ...area, right: e.activeLabel }; + }); + }, + [setSelectState], + ); + + const doZoom = useCallback(() => { + setSelectState(old => { + const { left, right } = old; + + if (left === undefined || right === undefined || left === right) { + // Either is undefined or both are same - zoom out + setZoomState({}); + } else if (left < right) { + setZoomState({ left, right }); + } else if (left > right) { + setZoomState({ left: right, right: left }); + } + + return {}; + }); + setShowSelection(false); + }, [setSelectState, setZoomState, setShowSelection]); + + const zoomProps = useMemo( + () => ({ + onMouseDown, + onMouseMove, + onMouseUp: doZoom, + }), + [onMouseDown, onMouseMove, doZoom], + ); + + useEffect(() => { + if (!showSelection) { + return undefined; + } + return registerSelection(setState); + }, [registerSelection, setState, showSelection]); + + const getZoomArea = useCallback( + (props?: ZoomAreaProps) => ( + + {showSelection && state.left && state.right ? ( + + ) : null} + + ), + [showSelection, state.left, state.right], + ); + + return { + zoomProps, + getZoomArea, + }; +} diff --git a/plugins/cicd-statistics/src/components/button-switch.tsx b/plugins/cicd-statistics/src/components/button-switch.tsx new file mode 100644 index 0000000000..65f55eee41 --- /dev/null +++ b/plugins/cicd-statistics/src/components/button-switch.tsx @@ -0,0 +1,140 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, MouseEvent } from 'react'; +import { ButtonGroup, Button, Tooltip, Zoom } from '@material-ui/core'; + +export interface SwitchValueDetails { + value: T; + tooltip?: string; + text?: string | JSX.Element; +} + +export type SwitchValue = T | SwitchValueDetails; + +export interface ButtonSwitchPropsBase { + values: ReadonlyArray>; + vertical?: boolean; +} +export interface ButtonSwitchPropsSingle + extends ButtonSwitchPropsBase { + multi?: false; + selection: T; + onChange: (selected: T) => void; +} +export interface ButtonSwitchPropsMulti + extends ButtonSwitchPropsBase { + multi: true; + selection: ReadonlyArray; + onChange: (selected: Array) => void; +} + +export type ButtonSwitchProps = + | ButtonSwitchPropsSingle + | ButtonSwitchPropsMulti; + +function switchValue(value: SwitchValue): T { + return typeof value === 'object' ? value.value : value; +} + +function switchText( + value: SwitchValue, +): string | JSX.Element { + return typeof value === 'object' ? value.text ?? value.value : value; +} + +function findParent(tagName: string, elem: HTMLElement): HTMLElement { + let node: HTMLElement | null = elem; + while (node.tagName !== tagName) { + node = node.parentElement; + if (!node) { + throw new Error(`Couldn't find ${tagName} parent`); + } + } + return node; +} + +export function ButtonSwitch(props: ButtonSwitchProps) { + const { values, vertical = false } = props; + + const onClick = useCallback( + (ev: MouseEvent) => { + const btn = findParent('BUTTON', ev.target as HTMLElement); + const index = [...btn.parentElement!.children].findIndex( + child => child === btn, + ); + const value = switchValue(values[index]); + + if (props.multi) { + props.onChange( + props.selection.includes(value as T) + ? props.selection.filter(val => val !== value) + : [...props.selection, value as T], + ); + } else { + props.onChange(value as T); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [values, props.selection, props.multi, props.onChange], + ); + + const hasSelection = (value: T) => { + if (props.multi) { + return props.selection.includes(value); + } + return props.selection === value; + }; + + const tooltipify = (value: SwitchValue, elem: JSX.Element) => + typeof value === 'object' && value.tooltip ? ( + + {elem} + + ) : ( + elem + ); + + return ( + + {values.map(value => + tooltipify( + value, + , + ), + )} + + ); +} diff --git a/plugins/cicd-statistics/src/components/chart-filters.tsx b/plugins/cicd-statistics/src/components/chart-filters.tsx new file mode 100644 index 0000000000..03cbea5dd0 --- /dev/null +++ b/plugins/cicd-statistics/src/components/chart-filters.tsx @@ -0,0 +1,497 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useState, useEffect, useMemo } from 'react'; +import { + Button, + Card, + CardHeader, + CardContent, + FormControl, + FormGroup, + FormControlLabel, + Grid, + Switch, + Tooltip, + Typography, + Theme, + makeStyles, +} from '@material-ui/core'; +import ShowChartIcon from '@material-ui/icons/ShowChart'; +import BarChartIcon from '@material-ui/icons/BarChart'; +import { + MuiPickersUtilsProvider, + KeyboardDatePicker, +} from '@material-ui/pickers'; +import { DateTime } from 'luxon'; +import LuxonUtils from '@date-io/luxon'; + +import { + ChartType, + ChartTypes, + CicdConfiguration, + CicdDefaults, + FilterBranchType, + FilterStatusType, + statusTypes, +} from '../apis/types'; +import { ChartableStagesAnalysis } from '../charts/types'; +import { ButtonSwitch, SwitchValue } from './button-switch'; +import { Toggle } from './toggle'; +import { DurationSlider } from './duration-slider'; +import { Label } from './label'; + +export const useStyles = makeStyles( + theme => ({ + rootCard: { + padding: theme.spacing(0, 0, 0, 0), + margin: theme.spacing(0, 0, 2, 0), + }, + updateButton: { + margin: theme.spacing(1, 0, 0, 0), + }, + header: { + margin: theme.spacing(0, 0, 0, 0), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + }, + title: { + margin: theme.spacing(3, 0, 1, 0), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + '&:first-child': { + margin: theme.spacing(1, 0, 1, 0), + }, + }, + buttonDescription: { + textTransform: 'uppercase', + margin: theme.spacing(1, 0, 0, 1), + }, + }), + { + name: 'CicdStatistics', + }, +); + +export type BranchSelection = FilterBranchType | 'all'; +export type StatusSelection = FilterStatusType; + +export interface ChartFilter { + fromDate: Date; + toDate: Date; + branch: string; + status: Array; +} + +export function getDefaultChartFilter( + cicdConfiguration: CicdConfiguration, +): ChartFilter { + const toDate = cicdConfiguration.defaults?.timeTo ?? new Date(); + return { + fromDate: + cicdConfiguration.defaults?.timeFrom ?? + DateTime.fromJSDate(toDate).minus({ months: 1 }).toJSDate(), + toDate, + branch: cicdConfiguration.defaults?.filterType ?? 'branch', + status: + cicdConfiguration.defaults?.filterStatus ?? + cicdConfiguration.availableStatuses.filter( + status => status === 'succeeded' || status === 'failed', + ), + }; +} + +function isSameChartFilter(a: ChartFilter, b: ChartFilter): boolean { + return ( + a.branch === b.branch && + [...a.status].sort().join(' ') === [...b.status].sort().join(' ') && + DateTime.fromJSDate(a.fromDate).hasSame( + DateTime.fromJSDate(b.fromDate), + 'day', + ) && + DateTime.fromJSDate(a.toDate).hasSame(DateTime.fromJSDate(b.toDate), 'day') + ); +} + +export type ViewOptions = Pick< + CicdDefaults, + | 'lowercaseNames' + | 'normalizeTimeRange' + | 'collapsedLimit' + | 'hideLimit' + | 'chartTypes' +>; + +export function getDefaultViewOptions( + cicdConfiguration: CicdConfiguration, +): ViewOptions { + return { + lowercaseNames: cicdConfiguration.defaults?.lowercaseNames ?? false, + normalizeTimeRange: cicdConfiguration.defaults?.normalizeTimeRange ?? true, + collapsedLimit: 60 * 1000, // 1m + hideLimit: 20 * 1000, // 20s + chartTypes: { + succeeded: ['duration'], + failed: ['count'], + enqueued: ['count'], + scheduled: ['count'], + running: ['count'], + aborted: ['count'], + stalled: ['count'], + expired: ['count'], + unknown: ['count'], + }, + }; +} + +const branchValues: Array> = [ + 'master', + 'branch', + { + value: 'all', + tooltip: + 'NOTE; If the build pipelines are very different between master and branch ' + + 'builds, viewing them combined might not result in a very useful chart', + }, +]; + +const chartTypeValues: Array> = [ + { value: 'duration', text: , tooltip: 'Duration' }, + { value: 'count', text: , tooltip: 'Count per day' }, +]; + +export interface ChartFiltersProps { + analysis?: ChartableStagesAnalysis; + + cicdConfiguration: CicdConfiguration; + initialFetchFilter: ChartFilter; + currentFetchFilter?: ChartFilter; + onChangeFetchFilter(filter: ChartFilter): void; + updateFetchFilter(filter: ChartFilter): void; + + initialViewOptions: ViewOptions; + onChangeViewOptions(filter: ViewOptions): void; +} + +interface InternalRef { + first: boolean; +} + +export function ChartFilters(props: ChartFiltersProps) { + const { + analysis, + cicdConfiguration, + initialFetchFilter, + currentFetchFilter, + onChangeFetchFilter, + updateFetchFilter, + initialViewOptions, + onChangeViewOptions, + } = props; + + const classes = useStyles(); + + const [internalRef] = useState({ first: true }); + + const [useNowAsToDate, setUseNowAsToDate] = useState(true); + const [toDate, setToDate] = useState(initialFetchFilter.toDate); + const [fromDate, setFromDate] = useState(initialFetchFilter.fromDate); + + const [branch, setBranch] = useState(initialFetchFilter.branch); + + const statusValues: ReadonlyArray = + cicdConfiguration.availableStatuses; + const [selectedStatus, setSelectedStatus] = useState( + initialFetchFilter.status, + ); + + const [viewOptions, setViewOptions] = useState(initialViewOptions); + + const setLowercaseNames = useCallback( + (lowercaseNames: boolean) => { + setViewOptions(old => ({ ...old, lowercaseNames })); + }, + [setViewOptions], + ); + + const setNormalizeTimeRange = useCallback( + (normalizeTimeRange: boolean) => { + setViewOptions(old => ({ ...old, normalizeTimeRange })); + }, + [setViewOptions], + ); + + const setHideLimit = useCallback( + (value: number) => { + setViewOptions(old => ({ ...old, hideLimit: value })); + }, + [setViewOptions], + ); + + const setCollapseLimit = useCallback( + (value: number) => { + setViewOptions(old => ({ ...old, collapsedLimit: value })); + }, + [setViewOptions], + ); + + const setChartType = useCallback( + (statusType: FilterStatusType, chartTypes: ChartTypes) => { + setViewOptions(old => ({ + ...old, + chartTypes: { ...old.chartTypes, [statusType]: chartTypes }, + })); + }, + [setViewOptions], + ); + const setChartTypeSpecific = useMemo( + () => + Object.fromEntries( + statusTypes.map( + status => + [ + status, + (chartTypes: ChartTypes) => setChartType(status, chartTypes), + ] as const, + ), + ), + [setChartType], + ); + + useEffect(() => { + onChangeViewOptions(viewOptions); + }, [onChangeViewOptions, viewOptions]); + + useEffect(() => { + if (internalRef.first) { + // Skip calling onChangeFetchFilter first time + internalRef.first = false; + return; + } + onChangeFetchFilter({ + toDate, + fromDate, + branch, + status: selectedStatus, + }); + }, [ + internalRef, + toDate, + fromDate, + branch, + selectedStatus, + onChangeFetchFilter, + ]); + + const toggleUseNowAsDate = useCallback(() => { + setUseNowAsToDate(!useNowAsToDate); + if (!DateTime.fromJSDate(toDate).hasSame(DateTime.now(), 'day')) { + setToDate(new Date()); + } + }, [useNowAsToDate, toDate]); + + const hasFetchFilterChanges = useMemo( + () => + !currentFetchFilter || + !isSameChartFilter( + { + toDate, + fromDate, + branch, + status: selectedStatus, + }, + currentFetchFilter, + ), + [toDate, fromDate, branch, selectedStatus, currentFetchFilter], + ); + + const updateFilter = useCallback(() => { + updateFetchFilter({ + toDate, + fromDate, + branch, + status: selectedStatus, + }); + }, [toDate, fromDate, branch, selectedStatus, updateFetchFilter]); + + const inrefferedStatuses = analysis?.statuses ?? selectedStatus; + + return ( + + + + Update + + } + title={ + + Fetching options + + } + /> + + + Date range + + setFromDate(date?.toJSDate() ?? new Date())} + /> +
+ + + + } + label={} + /> + {useNowAsToDate ? null : ( + setToDate(date?.toJSDate() ?? new Date())} + /> + )} + + + + Branch + + + values={branchValues} + selection={branch} + onChange={setBranch} + /> + + Status + + + values={statusValues} + multi + vertical + selection={selectedStatus} + onChange={setSelectedStatus} + /> +
+
+ + + View options + + } + /> + + + + + + + + + + + + + + + Chart styles + + {inrefferedStatuses.map(status => ( + + + + values={chartTypeValues} + selection={viewOptions.chartTypes[status as FilterStatusType]} + onChange={setChartTypeSpecific[status]} + multi + /> + + +
{status}
+
+
+ ))} +
+
+
+ ); +} diff --git a/plugins/cicd-statistics/src/components/duration-slider.tsx b/plugins/cicd-statistics/src/components/duration-slider.tsx new file mode 100644 index 0000000000..6ba4fa88ce --- /dev/null +++ b/plugins/cicd-statistics/src/components/duration-slider.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { Slider } from '@material-ui/core'; +import { debounce } from 'lodash'; + +import { formatDuration, formatDurationFromSeconds } from './utils'; +import { Label } from './label'; + +const marks = [ + 0, + 1, + 2, + 3, + 4, + 5, + 10, + 20, + 30, + 60, + 2 * 60, + 3 * 60, + 4 * 60, + 5 * 60, + 10 * 60, + 15 * 60, + 30 * 60, + 45 * 60, + 60 * 60, +].map((value, index) => ({ + value: index, + label: formatDurationFromSeconds(value), + seconds: value, +})); + +function findMarkIndex(seconds: number): number { + if (marks[0].seconds > seconds) { + return 0; + } else if (marks[marks.length - 1].seconds < seconds) { + return marks.length - 1; + } + for (let i = 0; i < marks.length - 1; ++i) { + const a = marks[i]; + const b = marks[i + 1]; + if (seconds === a.seconds) { + return i; + } else if (seconds === b.seconds) { + return i + 1; + } else if (a.seconds < seconds && b.seconds > seconds) { + return seconds - a.seconds < b.seconds - seconds ? i : i - 1; + } + } + return 0; // Won't happen +} + +function formatDurationFromIndex(index: number) { + return formatDurationFromSeconds(marks[index].seconds); +} + +export interface DurationSliderProps { + header: string; + value: number; + setValue: (value: number) => void; +} + +export function DurationSlider(props: DurationSliderProps) { + const { header, value, setValue } = props; + + const [curValue, setCurValue] = useState(value); + + const debouncedSetValue = useMemo(() => debounce(setValue, 1000), [setValue]); + + const onChange = useCallback( + (_: any, index: number | number[]) => { + const millis = marks[index as number].seconds * 1000; + setCurValue(millis); + debouncedSetValue(millis); + }, + [debouncedSetValue], + ); + + const indexValue = useMemo(() => findMarkIndex(curValue / 1000), [curValue]); + + return ( + <> + + + + ); +} diff --git a/plugins/cicd-statistics/src/components/label.tsx b/plugins/cicd-statistics/src/components/label.tsx new file mode 100644 index 0000000000..fe064d30a6 --- /dev/null +++ b/plugins/cicd-statistics/src/components/label.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { PropsWithChildren } from 'react'; +import { Typography, Theme, makeStyles } from '@material-ui/core'; + +export const useStyles = makeStyles( + theme => ({ + label: { + fontWeight: 'normal', + margin: theme.spacing(0), + }, + }), + { + name: 'CicdStatisticsLabel', + }, +); + +export function Label({ children }: PropsWithChildren<{}>) { + const classes = useStyles(); + + return ( + + {children} + + ); +} diff --git a/plugins/cicd-statistics/src/components/progress.tsx b/plugins/cicd-statistics/src/components/progress.tsx new file mode 100644 index 0000000000..6ea8e6497d --- /dev/null +++ b/plugins/cicd-statistics/src/components/progress.tsx @@ -0,0 +1,216 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { CSSProperties, DependencyList } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import { Box, LinearProgress } from '@material-ui/core'; +import Timeline from '@material-ui/lab/Timeline'; +import TimelineItem from '@material-ui/lab/TimelineItem'; +import TimelineSeparator from '@material-ui/lab/TimelineSeparator'; +import TimelineConnector from '@material-ui/lab/TimelineConnector'; +import TimelineContent from '@material-ui/lab/TimelineContent'; +import TimelineOppositeContent from '@material-ui/lab/TimelineOppositeContent'; +import TimelineDot, { TimelineDotProps } from '@material-ui/lab/TimelineDot'; +import Alert from '@material-ui/lab/Alert'; +import { useApp } from '@backstage/core-plugin-api'; + +const stepProgressStyle: CSSProperties = { + marginTop: 6, +}; + +// Matching react-use, only has loading/error/value +type AsyncState = + | { + loading: boolean; + error?: undefined; + value?: undefined; + } + | { + loading: true; + error?: Error | undefined; + value?: T; + } + | { + loading: false; + error: Error; + value?: undefined; + } + | { + loading: false; + error?: undefined; + value: T; + }; + +export interface ProgressStep extends ProgessAsSingle { + title: string; +} +export interface ProgessAsSteps { + steps: Array; +} +export interface ProgessAsSingle { + progress?: T; + progressBuffer?: T; +} + +export type ProgessState = + | ProgessAsSingle + | (T extends number ? ProgessAsSteps : { steps?: undefined }); + +export type ProgressAsLoading = ProgessState & { + loading: true; + error?: undefined; + value?: undefined; +}; +export type ProgressAsError = ProgessState & { + loading?: false | undefined; + error: Error; + value?: undefined; +}; +export type ProgressAsValue = ProgessState & { + loading?: false | undefined; + error?: undefined; + value: T; +}; + +/** + * An AsyncState but with the addition of progress (decimal 0-1) to allow + * rendering a progress bar while waiting. + */ +export type ProgressType = + | ProgressAsLoading + | ProgressAsError + | ProgressAsValue; + +const sentry = Symbol(); + +/** + * Casts an AsyncState or Progress into its non-succeeded sub types + */ +type Unsuccessful | AsyncState> = + S extends ProgressType + ? ProgressAsLoading | ProgressAsError + : Omit, 'value'>; + +/** + * Similar to useAsync except it "waits" for a dependent (upstream) async state + * to finish first, otherwise it forwards the dependent pending state. + * + * When/if the dependent state has settled successfully, the callback will be + * invoked for a new layer of async state with the dependent (upstream) success + * result as argument. + */ +export function useAsyncChain | AsyncState, R>( + parentState: S, + fn: (value: NonNullable) => Promise, + deps: DependencyList, +): Unsuccessful | AsyncState { + const childState = useAsync( + async () => (!parentState.value ? sentry : fn(parentState.value)), + [!parentState.error, !parentState.loading, parentState.value, ...deps], + ); + + if (!parentState.value) { + return parentState as Unsuccessful; + } else if (childState.value === sentry) { + return { loading: true }; + } + return childState as AsyncState; +} + +export function renderFallbacks( + state: ProgressType | AsyncState, + success: (value: T) => JSX.Element, +): JSX.Element { + if (state.loading) { + return ; + } else if (state.error) { + return {state.error.stack}; + } + + return success(state.value!); +} + +export function ViewProgress({ + state, +}: { + state: ProgressAsLoading | { loading: boolean }; +}) { + const { Progress } = useApp().getComponents(); + + const stateAsSingleProgress = state as ProgessAsSingle; + const stateAsStepProgress = state as ProgessAsSteps; + + if ( + !stateAsSingleProgress.progress && + !stateAsSingleProgress.progressBuffer && + !stateAsStepProgress.steps + ) { + // Simple spinner + return ; + } else if (stateAsSingleProgress.progress !== undefined) { + // Simple _single_ progress + return ( + + + + ); + } + + // Multi-step progresses + + return ( + + + {stateAsStepProgress.steps.map((step, index) => ( + + {step.title} + + + {index < stateAsStepProgress.steps.length - 1 ? ( + + ) : null} + + + {!step.progress && !step.progressBuffer ? null : ( + + )} + + + ))} + + + ); +} + +function getDotColor(step: ProgressStep): TimelineDotProps['color'] { + const progress = step.progress ?? 0; + + if (progress >= 1) { + return 'primary'; + } else if (progress > 0) { + return 'secondary'; + } + return 'grey'; +} diff --git a/plugins/cicd-statistics/src/components/toggle.tsx b/plugins/cicd-statistics/src/components/toggle.tsx new file mode 100644 index 0000000000..02f1941d85 --- /dev/null +++ b/plugins/cicd-statistics/src/components/toggle.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, PropsWithChildren } from 'react'; +import { FormControlLabel, Switch } from '@material-ui/core'; + +export interface ToggleProps { + checked: boolean; + setChecked: (checked: boolean) => void; +} + +export function Toggle({ + checked, + setChecked, + children, +}: PropsWithChildren) { + const toggler = useCallback(() => { + setChecked(!checked); + }, [checked, setChecked]); + + return ( + } + label={children} + /> + ); +} diff --git a/plugins/cicd-statistics/src/components/utils.tsx b/plugins/cicd-statistics/src/components/utils.tsx new file mode 100644 index 0000000000..0133cf7ba8 --- /dev/null +++ b/plugins/cicd-statistics/src/components/utils.tsx @@ -0,0 +1,133 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { CSSProperties } from 'react'; +import { DateTime, Duration } from 'luxon'; +import humanizeDuration from 'humanize-duration'; +import { capitalize } from 'lodash'; + +const infoText: CSSProperties = { color: 'InfoText' }; + +/** + * Picks {num} elements from {arr} evenly, from the first to the last + */ +export function pickElements(arr: ReadonlyArray, num: number): Array { + if (arr.length <= num) { + return [...arr]; + } + + if (num < 2) { + return [arr[arr.length / 2]]; + } + + const step = arr.length / (num - 1); + return [ + ...Array.from(Array(num - 1)).map( + (_, index) => arr[Math.round(index * step)], + ), + arr[arr.length - 1], + ]; +} + +function formatDateShort(milliseconds: number) { + if ((milliseconds as any) === 'auto') { + // When recharts gets confused (empty data) + return ''; + } + return DateTime.fromMillis(milliseconds).toLocaleString(DateTime.DATE_SHORT); +} +function formatDateTimeShort(milliseconds: number) { + if ((milliseconds as any) === 'auto') { + // When recharts gets confused (empty data) + return ''; + } + return DateTime.fromMillis(milliseconds).toLocaleString( + DateTime.DATETIME_SHORT, + ); +} + +export function labelFormatter(epoch: number) { + return {formatDateTimeShort(epoch)}; +} + +export function labelFormatterWithoutTime(epoch: number) { + return {formatDateShort(epoch)}; +} + +export function tickFormatterX(epoch: number) { + return formatDateShort(epoch); +} + +export function tickFormatterY(duration: number) { + if (duration === 0) { + return '0'; + } else if (duration < 500) { + return `${duration} ms`; + } + return formatDuration(duration) + .replace(/second.*/, 'sec') + .replace(/minute.*/, 'min') + .replace(/hour.*/, 'h') + .replace(/day.*/, 'd') + .replace(/month.*/, 'm') + .replace(/year.*/, 'y'); +} + +export function tooltipValueFormatter(durationOrCount: number, name: string) { + return [ + + {capitalize(name)}:{' '} + {name.endsWith(' count') + ? durationOrCount + : formatDuration(durationOrCount)} + , + null, + ]; +} + +export function formatDuration(millis: number) { + let rest = Math.round(millis); + const days = Math.floor(rest / (1000 * 60 * 60 * 24)); + rest -= days * (1000 * 60 * 60 * 24); + const hours = Math.floor(rest / (1000 * 60 * 60)); + rest -= hours * (1000 * 60 * 60); + const minutes = Math.floor(rest / (1000 * 60)); + rest -= minutes * (1000 * 60); + const seconds = Math.floor(rest / 1000); + rest -= seconds * 1000; + const milliseconds = rest; + + if (!days && !hours && !minutes) { + if (seconds < 1) { + return `${milliseconds}ms`; + } else if (seconds < 2) { + return `${((milliseconds + seconds * 1000) / 1000).toFixed(1)}s`; + } + } + + const dur = Duration.fromObject({ + ...(days && { days }), + ...(hours && { hours }), + ...(minutes && !days && { minutes }), + ...(seconds && !days && !hours && { seconds }), + }); + + return humanizeDuration(dur.toMillis(), { round: true }); +} + +export function formatDurationFromSeconds(seconds: number) { + return formatDuration(seconds * 1000); +} diff --git a/plugins/cicd-statistics/src/entity-page.tsx b/plugins/cicd-statistics/src/entity-page.tsx new file mode 100644 index 0000000000..3c51dabf2a --- /dev/null +++ b/plugins/cicd-statistics/src/entity-page.tsx @@ -0,0 +1,213 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useCallback, useState, useMemo, useEffect } from 'react'; +import { Grid, makeStyles, Theme } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { useApi, errorApiRef } from '@backstage/core-plugin-api'; +import { DateTime } from 'luxon'; + +import { + useCicdStatistics, + UseCicdStatisticsOptions, +} from './hooks/use-cicd-statistics'; +import { useCicdConfiguration } from './hooks/use-cicd-configuration'; +import { buildsToChartableStages } from './charts/logic/conversions'; +import { ZoomProvider, useZoom } from './charts/zoom'; +import { StageChart } from './charts/stage-chart'; +import { StatusChart } from './charts/status-chart'; +import { + ChartFilter, + ChartFilters, + getDefaultChartFilter, + getDefaultViewOptions, + ViewOptions, +} from './components/chart-filters'; +import { + CicdConfiguration, + FilterStatusType, + FilterBranchType, +} from './apis/types'; +import { cleanupBuildTree } from './utils/stage-names'; +import { renderFallbacks, useAsyncChain } from './components/progress'; +import { sortFilterStatusType } from './utils/api'; + +export function EntityPageCicdCharts() { + const state = useCicdConfiguration(); + + return renderFallbacks(state, value => ( + + + + )); +} + +const useStyles = makeStyles( + theme => ({ + pane: { + padding: theme.spacing(1, 1, 1, 1), + }, + }), + { + name: 'CicdStatisticsView', + }, +); + +function startOfDay(date: Date) { + return DateTime.fromJSDate(date).startOf('day').toJSDate(); +} +function endOfDay(date: Date) { + return DateTime.fromJSDate(date).endOf('day').toJSDate(); +} +function cleanChartFilter(filter: ChartFilter): ChartFilter { + return { + ...filter, + status: sortFilterStatusType(filter.status as FilterStatusType[]), + }; +} + +interface CicdChartsProps { + cicdConfiguration: CicdConfiguration; +} + +function CicdCharts(props: CicdChartsProps) { + const { cicdConfiguration } = props; + + const errorApi = useApi(errorApiRef); + const { entity } = useEntity(); + + const classes = useStyles(); + + const { resetZoom } = useZoom(); + + const [chartFilter, setChartFilter] = useState( + getDefaultChartFilter(cicdConfiguration), + ); + const [fetchedChartData, setFetchedChartData] = useState({ + abortController: null as null | AbortController, + chartFilter, + }); + + const [viewOptions, setViewOptions] = useState( + getDefaultViewOptions(cicdConfiguration), + ); + + const fetchStatisticsOptions = useMemo((): UseCicdStatisticsOptions => { + const abortController = new AbortController(); + fetchedChartData.abortController = abortController; + return { + abortController, + entity, + timeFrom: startOfDay(fetchedChartData.chartFilter.fromDate), + timeTo: endOfDay(fetchedChartData.chartFilter.toDate), + filterStatus: fetchedChartData.chartFilter.status as FilterStatusType[], + filterType: fetchedChartData.chartFilter.branch as FilterBranchType, + }; + }, [entity, fetchedChartData]); + + const statisticsState = useCicdStatistics(fetchStatisticsOptions); + + const updateFilter = useCallback(() => { + // Abort previous fetch + fetchedChartData.abortController?.abort(); + + setFetchedChartData({ abortController: null, chartFilter }); + }, [fetchedChartData, setFetchedChartData, chartFilter]); + + const chartableStagesState = useAsyncChain( + statisticsState, + async value => + buildsToChartableStages( + await cleanupBuildTree(value.builds, { + formatStageName: cicdConfiguration.formatStageName, + lowerCase: viewOptions.lowercaseNames, + }), + { normalizeTimeRange: viewOptions.normalizeTimeRange }, + ), + [statisticsState, cicdConfiguration, viewOptions], + ); + + useEffect(() => { + resetZoom(); + }, [resetZoom, statisticsState.value]); + + const onFilterChange = useCallback((filter: ChartFilter) => { + setChartFilter(cleanChartFilter(filter)); + }, []); + + const onViewOptionsChange = useCallback( + (options: ViewOptions) => { + setViewOptions(options); + }, + [setViewOptions], + ); + + useEffect(() => { + if ( + !chartableStagesState.error || + chartableStagesState.error?.name === 'AbortError' + ) { + return; + } + errorApi.post(chartableStagesState.error); + }, [errorApi, chartableStagesState.error]); + + return ( + + + + + + {renderFallbacks(chartableStagesState, chartableStages => ( + <> + {chartableStages.stages.size > 0 ? null : ( + No data + )} + {!statisticsState.value?.builds?.length || + !chartableStagesState.value?.daily?.values?.length ? null : ( + + )} + + {[...chartableStages.stages.entries()].map(([name, stage]) => ( + + ))} + + ))} + + + ); +} diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts b/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts new file mode 100644 index 0000000000..d21b978a92 --- /dev/null +++ b/plugins/cicd-statistics/src/hooks/use-cicd-configuration.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useState, useEffect } from 'react'; +import { useEntity } from '@backstage/plugin-catalog-react'; + +import { CicdConfiguration, statusTypes } from '../apis'; +import { ProgressType } from '../components/progress'; +import { defaultFormatStageName } from '../utils/stage-names'; +import { useCicdStatisticsApi } from './use-cicd-statistics-api'; + +export function useCicdConfiguration(): ProgressType { + const cicdStatisticsApi = useCicdStatisticsApi(); + const { entity } = useEntity(); + + const [state, setState] = useState>({ + loading: true, + }); + + useEffect(() => { + if (!cicdStatisticsApi) { + setState({ error: new Error('No CI/CD Statistics API installed') }); + return; + } + + cicdStatisticsApi + .getConfiguration({ entity }) + .then(configuration => { + const { + availableStatuses = statusTypes, + formatStageName = defaultFormatStageName, + defaults = {}, + } = configuration; + setState({ + value: { + availableStatuses, + formatStageName, + defaults, + }, + }); + }) + .catch(error => { + setState({ error }); + }); + }, [cicdStatisticsApi, entity]); + + return state; +} diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-statistics-api.ts b/plugins/cicd-statistics/src/hooks/use-cicd-statistics-api.ts new file mode 100644 index 0000000000..02773b20ef --- /dev/null +++ b/plugins/cicd-statistics/src/hooks/use-cicd-statistics-api.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useApi } from '@backstage/core-plugin-api'; + +import { cicdStatisticsApiRef } from '../apis'; + +export function useCicdStatisticsApi() { + try { + return useApi(cicdStatisticsApiRef); + } catch (err) { + return undefined; + } +} diff --git a/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts b/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts new file mode 100644 index 0000000000..e52673a576 --- /dev/null +++ b/plugins/cicd-statistics/src/hooks/use-cicd-statistics.ts @@ -0,0 +1,147 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useState, useEffect } from 'react'; +import { throttle } from 'lodash'; +import { Entity } from '@backstage/catalog-model'; + +import { + CicdState, + FetchBuildsOptions, + AbortError, + FilterStatusType, + FilterBranchType, + UpdateProgress, +} from '../apis'; +import { ProgressType } from '../components/progress'; +import { useCicdStatisticsApi } from './use-cicd-statistics-api'; + +export interface UseCicdStatisticsOptions { + entity: Entity; + abortController: AbortController; + timeFrom: Date; + timeTo: Date; + filterStatus: Array; + filterType: FilterBranchType | 'all'; +} + +export function useCicdStatistics( + options: UseCicdStatisticsOptions, +): ProgressType { + const { + entity, + abortController, + timeFrom, + timeTo, + filterStatus, + filterType, + } = options; + + const [state, setState] = useState>({ + loading: true, + }); + + const cicdStatisticsApi = useCicdStatisticsApi(); + + useEffect(() => { + if (!cicdStatisticsApi) { + setState({ error: new Error('No CI/CD Statistics API installed') }); + return () => {}; + } + + let mounted = true; + let completed = false; // successfully or failed + + const updateProgressImpl: UpdateProgress = (_count, _total?, _started?) => { + if (!mounted || completed) { + return; + } + + if (Array.isArray(_count)) { + // Multi-progress + setState({ + loading: true, + steps: _count.map(step => ({ + title: step.title, + progress: !step.total ? 0 : step.completed / step.total, + progressBuffer: !step.total ? 0 : (step.started ?? 0) / step.total, + })), + }); + } else { + // Single-progress + const count = _count; + const total = _total as number; + const started = (_started as number) ?? 0; + setState({ + loading: true, + progress: !total ? 0 : count / total, + progressBuffer: !total ? 0 : started / total, + }); + } + }; + + const updateProgress = throttle( + updateProgressImpl, + 200, + // throttle doesn't handle types of multi-signature functions + ) as any as UpdateProgress; + + const fetchOptions: FetchBuildsOptions = { + entity, + updateProgress, + abortSignal: abortController.signal, + timeFrom, + timeTo, + filterStatus, + filterType, + }; + + (async () => { + return cicdStatisticsApi.fetchBuilds(fetchOptions); + })() + .then(builds => { + completed = true; + if (mounted) { + setState({ + value: builds, + }); + } + }) + .catch(err => { + completed = true; + if (mounted) { + setState({ + error: abortController.signal.aborted ? new AbortError() : err, + }); + } + }); + + return () => { + mounted = false; + abortController.abort(); + }; + }, [ + abortController, + entity, + timeFrom, + timeTo, + filterStatus, + filterType, + cicdStatisticsApi, + ]); + + return state; +} diff --git a/plugins/cicd-statistics/src/index.ts b/plugins/cicd-statistics/src/index.ts new file mode 100644 index 0000000000..0190f37f20 --- /dev/null +++ b/plugins/cicd-statistics/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './plugin'; +export * from './apis'; diff --git a/plugins/cicd-statistics/src/plugin.ts b/plugins/cicd-statistics/src/plugin.ts new file mode 100644 index 0000000000..ca9506475a --- /dev/null +++ b/plugins/cicd-statistics/src/plugin.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createPlugin, + createRoutableExtension, + createRouteRef, +} from '@backstage/core-plugin-api'; + +export type { EntityPageCicdCharts } from './entity-page'; + +const rootCatalogCicdStatsRouteRef = createRouteRef({ + id: 'cicd-statistics', +}); + +export const cicdStatisticsPlugin = createPlugin({ + id: 'cicd-statistics', + routes: { + entityContent: rootCatalogCicdStatsRouteRef, + }, +}); + +export const EntityCicdStatisticsContent = cicdStatisticsPlugin.provide( + createRoutableExtension({ + component: () => import('./entity-page').then(m => m.EntityPageCicdCharts), + mountPoint: rootCatalogCicdStatsRouteRef, + name: 'EntityCicdStatisticsContent', + }), +); diff --git a/plugins/cicd-statistics/src/utils/api.ts b/plugins/cicd-statistics/src/utils/api.ts new file mode 100644 index 0000000000..8af21b3507 --- /dev/null +++ b/plugins/cicd-statistics/src/utils/api.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FilterStatusType, statusTypes } from '../apis/types'; + +export function sortFilterStatusType( + statuses: ReadonlyArray, +): Array { + const statusSet = new Set(statuses); + + const sorted = (['all', ...statusTypes] as Array).filter((status: T) => { + if (statusSet.has(status)) { + statusSet.delete(status); + return true; + } + return false; + }); + + return [...sorted, ...statusSet]; +} diff --git a/plugins/cicd-statistics/src/utils/stage-names.ts b/plugins/cicd-statistics/src/utils/stage-names.ts new file mode 100644 index 0000000000..69c8cdcf25 --- /dev/null +++ b/plugins/cicd-statistics/src/utils/stage-names.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { map } from 'already'; + +import { Build, Stage } from '../apis/types'; + +export function defaultFormatStageName( + parentNames: Array, + stageName: string, +): string { + let name = stageName; + + // Cut off parent names (if they are prefixed to the stage name) + parentNames.forEach(parentName => { + if (name.startsWith(parentName)) { + const newName = name + .slice(parentName.length) + // Remove things like ' - ' + .replace(/^[^\w\d]+/g, ''); + if (newName) { + name = newName; + } + } + }); + + // Cut off anything after colon in what looks like pulling docker images + return name.replace(/((pulling|(running)) [^:/]*\/.*?):.*/g, '$1'); +} + +export interface CleanupBuildTreeOptions { + formatStageName: typeof defaultFormatStageName; + lowerCase: boolean; +} + +export async function cleanupBuildTree( + builds: Build[], + opts: CleanupBuildTreeOptions, +): Promise { + const { formatStageName, lowerCase } = opts; + + const recurseStage = (stage: Stage, parentNames: Array): Stage => { + const name = formatStageName( + parentNames, + lowerCase ? stage.name.toLocaleLowerCase('en-US') : stage.name, + ); + const ancestry = [...parentNames, name]; + + return { + ...stage, + name, + stages: stage.stages?.map(subStage => recurseStage(subStage, ancestry)), + }; + }; + + return map(builds, { chunk: 'idle' }, build => ({ + ...build, + stages: build.stages.map(stage => recurseStage(stage, [])), + })); +} diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 1373040c3c..fcd6237d70 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-circleci +## 0.2.37 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.2.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.2.36 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.2.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.2.35 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 7a32e67b71..58df662e68 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-circleci", "description": "A Backstage plugin that integrates towards Circle CI", - "version": "0.2.35", + "version": "0.2.37", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,9 +33,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,17 +52,17 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/humanize-duration": "^3.25.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index f26431c739..8a434df33d 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-cloudbuild +## 0.2.35 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.2.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.2.34 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.2.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.2.33 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 3424d01ba9..7ed6f48f71 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cloudbuild", "description": "A Backstage plugin that integrates towards Google Cloud Build", - "version": "0.2.33", + "version": "0.2.35", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,16 +49,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 8faa0d789d..5e0bbb0c0f 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-code-coverage-backend +## 0.1.22 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/backend-common@0.10.7 + +## 0.1.22-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 433feaefa3..c5c77f9873 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.1.20", + "version": "0.1.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/catalog-client": "^0.5.5", + "@backstage/backend-common": "^0.10.7", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", @@ -30,13 +30,13 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-xml-bodyparser": "^0.3.0", - "knex": "^0.95.1", + "knex": "^1.0.2", "uuid": "^8.3.2", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/express-xml-bodyparser": "^0.3.2", "@types/supertest": "^2.0.8", "msw": "^0.35.0", diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index 06212ea885..2fc3acda84 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-code-coverage +## 0.1.25 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.1.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.1.24 + +### Patch Changes + +- 2ce5e4e0a7: Fixed a bug in the FileExplorer component which made it impossible to navigate upwards to a containing folder by clicking on the folder breadcrumb. +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.1.24-next.1 + +### Patch Changes + +- 2ce5e4e0a7: Fixed a bug in the FileExplorer component which made it impossible to navigate upwards to a containing folder by clicking on the folder breadcrumb. +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + - @backstage/plugin-catalog-react@0.6.13-next.1 + +## 0.1.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.1.23 ### Patch Changes diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index bf7ce4e794..49f092bf25 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.1.23", + "version": "0.1.25", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,10 +23,10 @@ "dependencies": { "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -43,10 +43,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -54,7 +54,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/recharts": "^1.8.15", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx index 4163b22ac1..b59b3ad1fd 100644 --- a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.test.tsx @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { groupByPath, buildFileStructure } from './FileExplorer'; +import { + groupByPath, + buildFileStructure, + getObjectsAtPath, +} from './FileExplorer'; const dummyFiles = [ { @@ -207,6 +211,28 @@ const coverageTableRowResults = { path: '', }; +const pathTestLeaf = { + files: [], + coverage: 1, + missing: 3, + tracked: 2, + path: 'file5', +}; + +const dummyFilesPathTest = { + files: [ + ...dummyFiles, + { + ...pathTestLeaf, + filename: 'dir3/dir7/file5', + }, + ], + coverage: 1, + missing: 1, + tracked: 1, + path: '', +}; + describe('groupByPath function', () => { it('should group files by their root directory,as per their filename', () => { expect(groupByPath(dummyFiles)).toStrictEqual(dummyDataGroupedByPath); @@ -220,3 +246,19 @@ describe('buildFileStructure function', () => { ); }); }); + +describe('getObjectsAtPath function', () => { + const structure = buildFileStructure(dummyFilesPathTest); + + it('should return the the dirs/files at the given path', () => { + expect(getObjectsAtPath(structure, ['dir3', 'dir7'])).toStrictEqual([ + pathTestLeaf, + ]); + }); + + it('should return undefined for a nonexistent path', () => { + expect( + getObjectsAtPath(structure, ['dir3', 'doesnt-exist']), + ).toBeUndefined(); + }); +}); diff --git a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx index 790de6e00f..3d41a1ff64 100644 --- a/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx +++ b/plugins/code-coverage/src/components/FileExplorer/FileExplorer.tsx @@ -131,6 +131,17 @@ const formatInitialData = (value: any) => { }); }; +export const getObjectsAtPath = ( + curData: CoverageTableRow | undefined, + path: string[], +): CoverageTableRow[] | undefined => { + let data = curData?.files; + for (const fragment of path) { + data = data?.find(d => d.path === fragment)?.files; + } + return data; +}; + export const FileExplorer = () => { const { entity } = useEntity(); const [curData, setCurData] = useState(); @@ -175,14 +186,10 @@ export const FileExplorer = () => { } }; - const moveUpIntoPath = (path: string) => { - const pathArray = path.split('/').filter(p => p.length); - let data = curData?.files; - pathArray.forEach(p => { - data = data?.find(d => d.path === p)?.files; - }); - setCurPath(path); - setTableData(data); + const moveUpIntoPath = (idx: number) => { + const path = curPath.split('/').slice(0, idx + 1); + setCurPath(path.join('/')); + setTableData(getObjectsAtPath(curData, path.slice(1))); }; const columns: TableColumn[] = [ @@ -270,8 +277,8 @@ export const FileExplorer = () => { color: `${idx !== lastPathElementIndex && 'lightblue'}`, cursor: `${idx !== lastPathElementIndex && 'pointer'}`, }} - onKeyDown={() => moveUpIntoPath(pathElement)} - onClick={() => moveUpIntoPath(pathElement)} + onKeyDown={() => moveUpIntoPath(idx)} + onClick={() => moveUpIntoPath(idx)} > {pathElement || 'root'}
diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index c83111f01b..a8b6f14292 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-config-schema +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.1.20 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.1.19 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index e1602227a6..46e030dbc8 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-config-schema", "description": "A Backstage plugin that lets you browse the configuration schema of your app", - "version": "0.1.19", + "version": "0.1.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,7 +22,7 @@ }, "dependencies": { "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", "@backstage/theme": "^0.2.14", @@ -38,16 +38,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 0144c3e74c..e1f3caf8ec 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-cost-insights +## 0.11.20 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.11.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.11.19 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.11.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.11.18 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 6ee59ee652..45178cb475 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cost-insights", "description": "A Backstage plugin that helps you keep track of your cloud spend", - "version": "0.11.18", + "version": "0.11.20", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,7 +33,7 @@ "dependencies": { "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -57,10 +57,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -71,7 +71,7 @@ "@types/regression": "^2.0.0", "@types/yup": "^0.29.13", "canvas": "^2.6.1", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json index 02705b3476..3da9de5e08 100644 --- a/plugins/explore-react/package.json +++ b/plugins/explore-react/package.json @@ -32,15 +32,15 @@ "@backstage/core-plugin-api": "^0.6.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2-next.0", + "@backstage/dev-utils": "^0.2.21-next.0", + "@backstage/test-utils": "^0.2.4-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 8e1487383f..d417976d1c 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-explore +## 0.3.29 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.3.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.3.28 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.3.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.3.27 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index f88ffef654..7cb22f5782 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore", "description": "A Backstage plugin for building an exploration page of your software ecosystem", - "version": "0.3.27", + "version": "0.3.29", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/plugin-explore-react": "^0.0.11", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -50,16 +50,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index 153f86f858..e446663842 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-firehydrant +## 0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.1.13 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 5a869b7f9b..81272e2b6d 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-firehydrant", "description": "A Backstage plugin that integrates towards FireHydrant", - "version": "0.1.13", + "version": "0.1.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,9 +22,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -36,16 +36,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index 3b429d6cca..6e42e95021 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-fossa +## 0.2.30 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.2.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.2.29 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.2.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.2.28 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 2a3160e4ab..53e40aae96 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-fossa", "description": "A Backstage plugin that integrates towards FOSSA", - "version": "0.2.28", + "version": "0.2.30", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,15 +33,15 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "luxon": "^2.0.2", "p-limit": "^3.0.2", "react-use": "^17.2.4" @@ -50,10 +50,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index ea8da0ec9f..80761a4736 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-gcp-projects +## 0.3.17 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.3.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.3.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.3.15 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index b9359af966..992336300f 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gcp-projects", "description": "A Backstage plugin that helps you manage projects in GCP", - "version": "0.3.15", + "version": "0.3.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -44,16 +44,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index cb074928b8..db988dbc9b 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-git-release-manager +## 0.3.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.3.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.3.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.3.9 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index dbc50e6921..381ca0dd24 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.3.9", + "version": "0.3.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,7 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/integration": "^0.7.2", "@backstage/theme": "^0.2.14", @@ -40,10 +40,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -51,7 +51,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/recharts": "^1.8.15", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/git-release-manager/src/features/Features.test.tsx b/plugins/git-release-manager/src/features/Features.test.tsx index 28bd9b6310..8f9e3544f2 100644 --- a/plugins/git-release-manager/src/features/Features.test.tsx +++ b/plugins/git-release-manager/src/features/Features.test.tsx @@ -55,7 +55,7 @@ describe('Features', () => { expect(getByTestId(TEST_IDS.info.info)).toMatchInlineSnapshot(`
JSX.Element; +// @public +export const HomePageStarredEntities: ( + props: ComponentRenderer & { + title?: string | undefined; + }, +) => JSX.Element; + // Warning: (ae-forgotten-export) The symbol "ToolkitContentProps" needs to be exported by the entry point index.d.ts // // @public diff --git a/plugins/home/package.json b/plugins/home/package.json index e29d48e6c1..80754d33c8 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-home", "description": "A Backstage plugin that helps you build a home page", - "version": "0.4.12", + "version": "0.4.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -10,6 +10,16 @@ "main": "dist/index.esm.js", "types": "dist/index.d.ts" }, + "homepage": "https://github.com/backstage/backstage/tree/master/plugins/home#readme", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/home" + }, + "keywords": [ + "backstage", + "homepage" + ], "scripts": { "build": "backstage-cli plugin:build", "start": "backstage-cli plugin:serve", @@ -21,10 +31,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/catalog-model": "^0.9.10", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.6.14", + "@backstage/plugin-search": "^0.6.2", "@backstage/theme": "^0.2.14", - "@backstage/plugin-search": "^0.6.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -37,16 +49,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx new file mode 100644 index 0000000000..ccc36f6964 --- /dev/null +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + renderInTestApp, + TestApiProvider, + MockStorageApi, +} from '@backstage/test-utils'; +import { + starredEntitiesApiRef, + entityRouteRef, + DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { Content } from './Content'; + +describe('StarredEntitiesContent', () => { + it('should render list of tools', async () => { + const mockStorageApi = MockStorageApi.create(); + await mockStorageApi + .forBucket('starredEntities') + .set('entityRefs', [ + 'component:default/mock-starred-entity', + 'component:default/mock-starred-entity-2', + ]); + + const { getByText } = await renderInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(getByText('mock-starred-entity')).toBeInTheDocument(); + expect(getByText('mock-starred-entity-2')).toBeInTheDocument(); + expect(getByText('mock-starred-entity').closest('a')).toHaveAttribute( + 'href', + '/catalog/default/component/mock-starred-entity', + ); + expect(getByText('mock-starred-entity-2').closest('a')).toHaveAttribute( + 'href', + '/catalog/default/component/mock-starred-entity-2', + ); + }); +}); diff --git a/plugins/home/src/homePageComponents/StarredEntities/Content.tsx b/plugins/home/src/homePageComponents/StarredEntities/Content.tsx new file mode 100644 index 0000000000..6c9775c842 --- /dev/null +++ b/plugins/home/src/homePageComponents/StarredEntities/Content.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + useStarredEntities, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { parseEntityRef } from '@backstage/catalog-model'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import { Link } from '@backstage/core-components'; +import { + List, + ListItem, + ListItemSecondaryAction, + IconButton, + ListItemText, + Tooltip, + Typography, +} from '@material-ui/core'; +import StarIcon from '@material-ui/icons/Star'; +import React from 'react'; + +/** + * A component to display a list of starred entities for the user. + * + * @public + */ + +export const Content = () => { + const catalogEntityRoute = useRouteRef(entityRouteRef); + const { starredEntities, toggleStarredEntity } = useStarredEntities(); + + if (starredEntities.size === 0) + return ( + + You do not have any starred entities yet! + + ); + + return ( + + {Array.from(starredEntities).map(entity => ( + + + + + + + toggleStarredEntity(entity)} + > + + + + + + ))} + + ); +}; diff --git a/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx new file mode 100644 index 0000000000..2e63762369 --- /dev/null +++ b/plugins/home/src/homePageComponents/StarredEntities/StarredEntities.stories.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HomePageStarredEntities } from '../../plugin'; +import { + wrapInTestApp, + TestApiProvider, + MockStorageApi, +} from '@backstage/test-utils'; +import { + starredEntitiesApiRef, + entityRouteRef, + DefaultStarredEntitiesApi, +} from '@backstage/plugin-catalog-react'; +import { Grid } from '@material-ui/core'; +import React, { ComponentType } from 'react'; + +const mockStorageApi = MockStorageApi.create(); +mockStorageApi + .forBucket('starredEntities') + .set('entityRefs', [ + 'component:default/example-starred-entity', + 'component:default/example-starred-entity-2', + 'component:default/example-starred-entity-3', + 'component:default/example-starred-entity-4', + ]); + +export default { + title: 'Plugins/Home/Components/StarredEntities', + decorators: [ + (Story: ComponentType<{}>) => + wrapInTestApp( + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ), + ], +}; + +export const Default = () => { + return ( + + + + ); +}; diff --git a/plugins/home/src/homePageComponents/StarredEntities/index.ts b/plugins/home/src/homePageComponents/StarredEntities/index.ts new file mode 100644 index 0000000000..1faa9a2426 --- /dev/null +++ b/plugins/home/src/homePageComponents/StarredEntities/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { Content } from './Content'; diff --git a/plugins/home/src/index.ts b/plugins/home/src/index.ts index dcd61f1fa3..4871323677 100644 --- a/plugins/home/src/index.ts +++ b/plugins/home/src/index.ts @@ -26,6 +26,7 @@ export { HomePageRandomJoke, HomePageToolkit, HomePageCompanyLogo, + HomePageStarredEntities, ComponentAccordion, ComponentTabs, ComponentTab, diff --git a/plugins/home/src/plugin.ts b/plugins/home/src/plugin.ts index 135ac97af0..32005a9064 100644 --- a/plugins/home/src/plugin.ts +++ b/plugins/home/src/plugin.ts @@ -115,3 +115,16 @@ export const HomePageToolkit = homePlugin.provide( components: () => import('./homePageComponents/Toolkit'), }), ); + +/** + * A component to display a list of starred entities for the user. + * + * @public + */ +export const HomePageStarredEntities = homePlugin.provide( + createCardExtension({ + name: 'HomePageStarredEntities', + title: 'Your Starred Entities', + components: () => import('./homePageComponents/StarredEntities'), + }), +); diff --git a/plugins/home/src/templates/DefaultTemplate.stories.tsx b/plugins/home/src/templates/DefaultTemplate.stories.tsx index fb62591fbd..ba5e1f33fd 100644 --- a/plugins/home/src/templates/DefaultTemplate.stories.tsx +++ b/plugins/home/src/templates/DefaultTemplate.stories.tsx @@ -14,20 +14,38 @@ * limitations under the License. */ -import {TemplateBackstageLogo} from './TemplateBackstageLogo'; -import {TemplateBackstageLogoIcon} from './TemplateBackstageLogoIcon'; -import { HomePageToolkit, HomePageCompanyLogo } from '../plugin'; -import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { TemplateBackstageLogo } from './TemplateBackstageLogo'; +import { TemplateBackstageLogoIcon } from './TemplateBackstageLogoIcon'; +import { + HomePageToolkit, + HomePageCompanyLogo, + HomePageStarredEntities, +} from '../plugin'; +import { wrapInTestApp, TestApiProvider, MockStorageApi} from '@backstage/test-utils'; import { Content, Page, InfoCard } from '@backstage/core-components'; +import { + starredEntitiesApiRef, + entityRouteRef, + DefaultStarredEntitiesApi +} from '@backstage/plugin-catalog-react'; import { HomePageSearchBar, SearchContextProvider, searchApiRef, - searchPlugin + searchPlugin, } from '@backstage/plugin-search'; import { Grid, makeStyles } from '@material-ui/core'; -import React, { ComponentType} from 'react'; +import React, { ComponentType } from 'react'; +const mockStorageApi = MockStorageApi.create(); +mockStorageApi + .forBucket('starredEntities') + .set('entityRefs', [ + 'component:default/example-starred-entity', + 'component:default/example-starred-entity-2', + 'component:default/example-starred-entity-3', + 'component:default/example-starred-entity-4' + ]); export default { title: 'Plugins/Home/Templates', @@ -35,13 +53,26 @@ export default { (Story: ComponentType<{}>) => wrapInTestApp( <> - Promise.resolve({results: []})}]]}> - + Promise.resolve({ results: [] }) }], + ]} + > + , { - mountedRoutes: {'/hello-company': searchPlugin.routes.root } - } + mountedRoutes: { + '/hello-company': searchPlugin.routes.root, + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ), ], }; @@ -82,20 +113,17 @@ export const DefaultTemplate = () => { } + logo={} /> - - {/* placeholder for content */} -
- + JSX.Element; + // Warning: (ae-missing-release-tag) "EntityNewRelicDashboardCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index a4899865e2..dba9cc1148 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic-dashboard", - "version": "0.1.4", + "version": "0.1.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,21 +21,21 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/dev-utils": "^0.2.19", + "@backstage/cli": "^0.13.2", + "@backstage/dev-utils": "^0.2.21", "@testing-library/jest-dom": "^5.10.1", "@types/react": "^16.13.1 || ^17.0.0", - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.1.5" }, "files": [ "dist" diff --git a/plugins/newrelic-dashboard/src/index.ts b/plugins/newrelic-dashboard/src/index.ts index 821ccd4bda..060306524f 100644 --- a/plugins/newrelic-dashboard/src/index.ts +++ b/plugins/newrelic-dashboard/src/index.ts @@ -17,5 +17,6 @@ export { newRelicDashboardPlugin, EntityNewRelicDashboardCard, EntityNewRelicDashboardContent, + DashboardSnapshotComponent, } from './plugin'; export { isNewRelicDashboardAvailable } from './Router'; diff --git a/plugins/newrelic-dashboard/src/plugin.ts b/plugins/newrelic-dashboard/src/plugin.ts index 18397902f7..9bfecff18f 100644 --- a/plugins/newrelic-dashboard/src/plugin.ts +++ b/plugins/newrelic-dashboard/src/plugin.ts @@ -60,3 +60,22 @@ export const EntityNewRelicDashboardCard = newRelicDashboardPlugin.provide( }, }), ); +/** + * Render dashboard snapshots from Newrelic in backstage. Use dashboards which have the tag `isDashboardPage: true` + * + * @remarks + * This can be helpful for rendering dashboards outside of Entity Catalog. + * + * @public + */ +export const DashboardSnapshotComponent = newRelicDashboardPlugin.provide( + createComponentExtension({ + name: 'DashboardSnapshotComponent', + component: { + lazy: () => + import( + './components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshot' + ).then(m => m.DashboardSnapshot), + }, + }), +); diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index 87feb883dd..821de4582a 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-newrelic +## 0.3.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.3.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.3.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.3.14 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 48a5050ad6..9f9d51eae3 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-newrelic", "description": "A Backstage plugin that integrates towards New Relic", - "version": "0.3.14", + "version": "0.3.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -44,16 +44,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index d7a4ed8e42..e9dfbce07d 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-org +## 0.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.4.1 + +### Patch Changes + +- ef86143c16: Added `relations.memberof` filter to the catalog api call in `MemberListCard` to avoid fetching all the User entity kinds from catalog-backend. +- 64cbca7839: The description from `metadata.description` will now show as the `subheader` on the UserProfileCard in the same way as the GroupProfileCard +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.4.1-next.0 + +### Patch Changes + +- ef86143c16: Added `relations.memberof` filter to the catalog api call in `MemberListCard` to avoid fetching all the User entity kinds from catalog-backend. +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.4.0 ### Minor Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index 0994e0dbfa..947d5e8a0b 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.4.0", + "version": "0.4.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,9 +22,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,17 +39,17 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/catalog-client": "^0.5.5", - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/catalog-client": "^0.6.0", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index 26a79dfaee..2030f4c100 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -15,8 +15,8 @@ */ import { - CatalogEntitiesRequest, - CatalogListResponse, + GetEntitiesRequest, + GetEntitiesResponse, } from '@backstage/catalog-client'; import { Entity, GroupEntity, UserEntity } from '@backstage/catalog-model'; import { @@ -110,15 +110,15 @@ const items = [ ] as Entity[]; const getEntitiesMock = ( - request?: CatalogEntitiesRequest, -): Promise> => { + request?: GetEntitiesRequest, +): Promise => { const filterKinds = !Array.isArray(request?.filter) && Array.isArray(request?.filter?.kind) ? request?.filter?.kind ?? [] : []; // we expect the request to be like { filter: { kind: ['API','System'], .... }. If changed in OwnerShipCard, let's change in also here return Promise.resolve({ items: items.filter(item => filterKinds.find(k => k === item.kind)), - } as CatalogListResponse); + } as GetEntitiesResponse); }; describe('OwnershipCard', () => { diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx index aecc401f52..0cb3987c77 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx @@ -40,6 +40,7 @@ const defaultEntity: UserEntity = { kind: 'User', metadata: { name: 'guest', + description: 'Description for guest', }, spec: { profile: { @@ -70,6 +71,7 @@ const noImageEntity: UserEntity = { kind: 'User', metadata: { name: 'guest', + description: 'Description for guest', }, spec: { profile: { diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx index 743ed987fa..7b9904954f 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx @@ -29,6 +29,7 @@ describe('UserSummary Test', () => { kind: 'User', metadata: { name: 'calum.leavy', + description: 'Super awesome human', }, spec: { profile: { @@ -73,5 +74,6 @@ describe('UserSummary Test', () => { 'href', '/catalog/default/group/ExampleGroup', ); + expect(rendered.getByText('Super awesome human')).toBeInTheDocument(); }); }); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index 298819ed09..0997e0a4d8 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -61,7 +61,7 @@ export const UserProfileCard = ({ } const { - metadata: { name: metaName }, + metadata: { name: metaName, description }, spec: { profile }, } = user; const displayName = profile?.displayName ?? metaName; @@ -71,7 +71,11 @@ export const UserProfileCard = ({ }); return ( - } variant={variant}> + } + subheader={description} + variant={variant} + > diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index 5cccf1debe..6fbcfc54b1 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-pagerduty +## 0.3.25 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.3.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.3.24 + +### Patch Changes + +- 5a459626bc: Fix change events tab error when change events exist +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.3.24-next.0 + +### Patch Changes + +- 5a459626bc: Fix change events tab error when change events exist +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.3.23 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index c01c2ce2ba..7912a32f15 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.3.23", + "version": "0.3.25", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,16 +49,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 7719376104..58ace0ff17 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-permission-backend +## 0.4.3 + +### Patch Changes + +- b3f3e42036: Use `getBearerTokenFromAuthorizationHeader` from `@backstage/plugin-auth-node` instead of the deprecated `IdentityClient` method. +- Updated dependencies + - @backstage/backend-common@0.10.7 + - @backstage/plugin-auth-node@0.1.0 + - @backstage/plugin-permission-node@0.4.3 + +## 0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.10.0-next.0 + - @backstage/backend-common@0.10.7-next.0 + - @backstage/plugin-permission-node@0.4.3-next.0 + +## 0.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0 + - @backstage/backend-common@0.10.6 + - @backstage/plugin-permission-node@0.4.2 + +## 0.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0-next.1 + - @backstage/backend-common@0.10.6-next.0 + - @backstage/plugin-permission-node@0.4.2-next.1 + +## 0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0-next.0 + - @backstage/plugin-permission-node@0.4.2-next.0 + ## 0.4.1 ### Patch Changes diff --git a/plugins/permission-backend/api-report.md b/plugins/permission-backend/api-report.md index 4857c8b044..2b59113535 100644 --- a/plugins/permission-backend/api-report.md +++ b/plugins/permission-backend/api-report.md @@ -3,8 +3,9 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { Config } from '@backstage/config'; import express from 'express'; -import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { IdentityClient } from '@backstage/plugin-auth-node'; import { Logger as Logger_2 } from 'winston'; import { PermissionPolicy } from '@backstage/plugin-permission-node'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; @@ -14,6 +15,8 @@ export function createRouter(options: RouterOptions): Promise; // @public export interface RouterOptions { + // (undocumented) + config: Config; // (undocumented) discovery: PluginEndpointDiscovery; // (undocumented) diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index 691d7eeefc..e1a4e10c5c 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.4.1", + "version": "0.4.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,24 +19,24 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", - "@backstage/plugin-auth-backend": "^0.8.0", + "@backstage/plugin-auth-node": "^0.1.0", "@backstage/plugin-permission-common": "^0.4.0", - "@backstage/plugin-permission-node": "^0.4.1", + "@backstage/plugin-permission-node": "^0.4.3", "@types/express": "*", "dataloader": "^2.0.0", "express": "^4.17.1", "express-promise-router": "^4.1.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "winston": "^3.2.1", "yn": "^4.0.0", "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", "supertest": "^6.1.6", diff --git a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts index 4be1b55379..9981496658 100644 --- a/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts +++ b/plugins/permission-backend/src/service/PermissionIntegrationClient.test.ts @@ -20,7 +20,11 @@ import express, { Router, RequestHandler } from 'express'; import { RestContext, rest } from 'msw'; import { setupServer, SetupServerApi } from 'msw/node'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { + AuthorizeResult, + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node'; import { PermissionIntegrationClient } from './PermissionIntegrationClient'; @@ -28,7 +32,7 @@ describe('PermissionIntegrationClient', () => { describe('applyConditions', () => { let server: SetupServerApi; - const mockConditions = { + const mockConditions: PermissionCriteria = { not: { allOf: [ { rule: 'RULE_1', params: [] }, diff --git a/plugins/permission-backend/src/service/router.test.ts b/plugins/permission-backend/src/service/router.test.ts index fd09cca828..b98b32b01c 100644 --- a/plugins/permission-backend/src/service/router.test.ts +++ b/plugins/permission-backend/src/service/router.test.ts @@ -17,7 +17,7 @@ import express from 'express'; import request from 'supertest'; import { getVoidLogger } from '@backstage/backend-common'; -import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { IdentityClient } from '@backstage/plugin-auth-node'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ApplyConditionsRequestEntry, @@ -26,6 +26,7 @@ import { import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { createRouter } from './router'; +import { ConfigReader } from '@backstage/config'; const mockApplyConditions: jest.MockedFunction< InstanceType['applyConditions'] @@ -63,6 +64,7 @@ describe('createRouter', () => { beforeAll(async () => { const router = await createRouter({ + config: new ConfigReader({ permission: { enabled: true } }), logger: getVoidLogger(), discovery: { getBaseUrl: jest.fn(), diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index e22aec8129..48f7b1a2e5 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -24,9 +24,10 @@ import { } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import { + getBearerTokenFromAuthorizationHeader, BackstageIdentityResponse, IdentityClient, -} from '@backstage/plugin-auth-backend'; +} from '@backstage/plugin-auth-node'; import { AuthorizeResult, AuthorizeDecision, @@ -43,6 +44,7 @@ import { import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { memoize } from 'lodash'; import DataLoader from 'dataloader'; +import { Config } from '@backstage/config'; const querySchema: z.ZodSchema> = z.object({ id: z.string(), @@ -78,6 +80,7 @@ export interface RouterOptions { discovery: PluginEndpointDiscovery; policy: PermissionPolicy; identity: IdentityClient; + config: Config; } const handleRequest = async ( @@ -138,7 +141,13 @@ const handleRequest = async ( export async function createRouter( options: RouterOptions, ): Promise { - const { policy, discovery, identity } = options; + const { policy, discovery, identity, config, logger } = options; + + if (!config.getOptionalBoolean('permission.enabled')) { + logger.warn( + 'Permission backend started with permissions disabled. Enable permissions by setting permission.enabled=true.', + ); + } const permissionIntegrationClient = new PermissionIntegrationClient({ discovery, @@ -157,7 +166,9 @@ export async function createRouter( req: Request, res: Response, ) => { - const token = IdentityClient.getBearerToken(req.header('authorization')); + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); const user = token ? await identity.authenticate(token) : undefined; const parseResult = requestSchema.safeParse(req.body); diff --git a/plugins/permission-common/api-report.md b/plugins/permission-common/api-report.md index 30a67e8c8b..db05766dc0 100644 --- a/plugins/permission-common/api-report.md +++ b/plugins/permission-common/api-report.md @@ -5,6 +5,16 @@ ```ts import { Config } from '@backstage/config'; +// @public +export type AllOfCriteria = { + allOf: NonEmptyArray>; +}; + +// @public +export type AnyOfCriteria = { + anyOf: NonEmptyArray>; +}; + // @public export type AuthorizeDecision = | { @@ -65,6 +75,11 @@ export function isReadPermission(permission: Permission): boolean; // @public export function isUpdatePermission(permission: Permission): boolean; +// @public +export type NotCriteria = { + not: PermissionCriteria; +}; + // @public export type Permission = { name: string; @@ -103,14 +118,8 @@ export type PermissionCondition = { // @public export type PermissionCriteria = - | { - allOf: PermissionCriteria[]; - } - | { - anyOf: PermissionCriteria[]; - } - | { - not: PermissionCriteria; - } + | AllOfCriteria + | AnyOfCriteria + | NotCriteria | TQuery; ``` diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index 69af3054c0..d3054fe9fe 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -40,12 +40,12 @@ "dependencies": { "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "uuid": "^8.0.0", "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2-next.0", "@types/jest": "^26.0.7", "msw": "^0.35.0" } diff --git a/plugins/permission-common/src/PermissionClient.ts b/plugins/permission-common/src/PermissionClient.ts index 58f4d44d20..9431050690 100644 --- a/plugins/permission-common/src/PermissionClient.ts +++ b/plugins/permission-common/src/PermissionClient.ts @@ -43,9 +43,18 @@ const permissionCriteriaSchema: z.ZodSchema< rule: z.string(), params: z.array(z.unknown()), }) - .or(z.object({ anyOf: z.array(permissionCriteriaSchema) })) - .or(z.object({ allOf: z.array(permissionCriteriaSchema) })) - .or(z.object({ not: permissionCriteriaSchema })), + .strict() + .or( + z + .object({ anyOf: z.array(permissionCriteriaSchema).nonempty() }) + .strict(), + ) + .or( + z + .object({ allOf: z.array(permissionCriteriaSchema).nonempty() }) + .strict(), + ) + .or(z.object({ not: permissionCriteriaSchema }).strict()), ); const responseSchema = z.object({ diff --git a/plugins/permission-common/src/types/api.ts b/plugins/permission-common/src/types/api.ts index 287c7ffe83..e0ab1f1642 100644 --- a/plugins/permission-common/src/types/api.ts +++ b/plugins/permission-common/src/types/api.ts @@ -72,14 +72,44 @@ export type PermissionCondition = { params: TParams; }; +/** + * Utility type to represent an array with 1 or more elements. + * @ignore + */ +type NonEmptyArray = [T, ...T[]]; + +/** + * Represnts a logical AND for the provided criteria. + * @public + */ +export type AllOfCriteria = { + allOf: NonEmptyArray>; +}; + +/** + * Represnts a logical OR for the provided criteria. + * @public + */ +export type AnyOfCriteria = { + anyOf: NonEmptyArray>; +}; + +/** + * Represents a negation of the provided criteria. + * @public + */ +export type NotCriteria = { + not: PermissionCriteria; +}; + /** * Composes several {@link PermissionCondition}s as criteria with a nested AND/OR structure. * @public */ export type PermissionCriteria = - | { allOf: PermissionCriteria[] } - | { anyOf: PermissionCriteria[] } - | { not: PermissionCriteria } + | AllOfCriteria + | AnyOfCriteria + | NotCriteria | TQuery; /** diff --git a/plugins/permission-common/src/types/index.ts b/plugins/permission-common/src/types/index.ts index b4453fcc63..fa98de73f1 100644 --- a/plugins/permission-common/src/types/index.ts +++ b/plugins/permission-common/src/types/index.ts @@ -23,6 +23,9 @@ export type { Identified, PermissionCondition, PermissionCriteria, + AllOfCriteria, + AnyOfCriteria, + NotCriteria, } from './api'; export type { DiscoveryApi } from './discovery'; export type { diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index a552e2c67b..d79c79159f 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-permission-node +## 0.4.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + - @backstage/plugin-auth-node@0.1.0 + +## 0.4.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.10.0-next.0 + - @backstage/backend-common@0.10.7-next.0 + +## 0.4.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0 + - @backstage/backend-common@0.10.6 + +## 0.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0-next.1 + - @backstage/backend-common@0.10.6-next.0 + +## 0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0-next.0 + ## 0.4.1 ### Patch Changes diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 43bb548a06..534ad45110 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -7,7 +7,7 @@ import { AuthorizeDecision } from '@backstage/plugin-permission-common'; import { AuthorizeQuery } from '@backstage/plugin-permission-common'; import { AuthorizeRequestOptions } from '@backstage/plugin-permission-common'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-backend'; +import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { Config } from '@backstage/config'; import express from 'express'; import { Identified } from '@backstage/plugin-permission-common'; diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 11fb27f34e..04a567f16b 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.4.1", + "version": "0.4.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", - "@backstage/plugin-auth-backend": "^0.8.0", + "@backstage/plugin-auth-node": "^0.1.0", "@backstage/plugin-permission-common": "^0.4.0", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -40,7 +40,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/permission-node/src/integration/createConditionTransformer.ts b/plugins/permission-node/src/integration/createConditionTransformer.ts index 0e736b832c..ee4224b8bd 100644 --- a/plugins/permission-node/src/integration/createConditionTransformer.ts +++ b/plugins/permission-node/src/integration/createConditionTransformer.ts @@ -14,6 +14,8 @@ * limitations under the License. */ import { + AllOfCriteria, + AnyOfCriteria, PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; @@ -32,11 +34,11 @@ const mapConditions = ( if (isAndCriteria(criteria)) { return { allOf: criteria.allOf.map(child => mapConditions(child, getRule)), - }; + } as AllOfCriteria; } else if (isOrCriteria(criteria)) { return { anyOf: criteria.anyOf.map(child => mapConditions(child, getRule)), - }; + } as AnyOfCriteria; } else if (isNotCriteria(criteria)) { return { not: mapConditions(criteria.not, getRule), diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts index 065c30a978..6408de46a1 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -247,7 +247,8 @@ describe('createPermissionIntegrationRouter', () => { resourceRef: 'default:test/resource-1', resourceType: 'test-incorrect-resource-1', conditions: { - anyOf: [], + rule: 'test-rule-1', + params: [{}], }, }, { @@ -255,7 +256,8 @@ describe('createPermissionIntegrationRouter', () => { resourceRef: 'default:test/resource-2', resourceType: 'test-resource', conditions: { - anyOf: [], + rule: 'test-rule-1', + params: [{}], }, }, { @@ -263,7 +265,8 @@ describe('createPermissionIntegrationRouter', () => { resourceRef: 'default:test/resource-3', resourceType: 'test-incorrect-resource-2', conditions: { - anyOf: [], + rule: 'test-rule-1', + params: [{}], }, }, ], diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 1b01660fb0..efb5d7c3a6 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -38,13 +38,15 @@ const permissionCriteriaSchema: z.ZodSchema< PermissionCriteria > = z.lazy(() => z.union([ - z.object({ anyOf: z.array(permissionCriteriaSchema) }), - z.object({ allOf: z.array(permissionCriteriaSchema) }), - z.object({ not: permissionCriteriaSchema }), - z.object({ - rule: z.string(), - params: z.array(z.unknown()), - }), + z.object({ anyOf: z.array(permissionCriteriaSchema).nonempty() }).strict(), + z.object({ allOf: z.array(permissionCriteriaSchema).nonempty() }).strict(), + z.object({ not: permissionCriteriaSchema }).strict(), + z + .object({ + rule: z.string(), + params: z.array(z.unknown()), + }) + .strict(), ]), ); diff --git a/plugins/permission-node/src/integration/util.ts b/plugins/permission-node/src/integration/util.ts index d9457cfefc..3878e18895 100644 --- a/plugins/permission-node/src/integration/util.ts +++ b/plugins/permission-node/src/integration/util.ts @@ -14,22 +14,27 @@ * limitations under the License. */ -import { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { + AllOfCriteria, + AnyOfCriteria, + NotCriteria, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; import { PermissionRule } from '../types'; export const isAndCriteria = ( filter: PermissionCriteria, -): filter is { allOf: PermissionCriteria[] } => +): filter is AllOfCriteria => Object.prototype.hasOwnProperty.call(filter, 'allOf'); export const isOrCriteria = ( filter: PermissionCriteria, -): filter is { anyOf: PermissionCriteria[] } => +): filter is AnyOfCriteria => Object.prototype.hasOwnProperty.call(filter, 'anyOf'); export const isNotCriteria = ( filter: PermissionCriteria, -): filter is { not: PermissionCriteria } => +): filter is NotCriteria => Object.prototype.hasOwnProperty.call(filter, 'not'); export const createGetRule = ( diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index 2e344d6d96..19b12b8f28 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -20,7 +20,7 @@ import { PermissionCondition, PermissionCriteria, } from '@backstage/plugin-permission-common'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-backend'; +import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; /** * An authorization request to be evaluated by the {@link PermissionPolicy}. diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index dd3a2c696f..ce6e2dbf54 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -30,17 +30,18 @@ "@backstage/config": "^0.1.13", "@backstage/core-plugin-api": "^0.6.0", "@backstage/plugin-permission-common": "^0.4.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "react-router": "6.0.0-beta.0", - "react-use": "^17.2.4" + "react-use": "^17.2.4", + "swr": "^1.1.2" }, "peerDependencies": { "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2-next.0", + "@backstage/test-utils": "^0.2.4-next.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@types/jest": "^26.0.7" diff --git a/plugins/permission-react/src/hooks/usePermission.test.tsx b/plugins/permission-react/src/hooks/usePermission.test.tsx index 4cf0bc6530..d6c5b2ed33 100644 --- a/plugins/permission-react/src/hooks/usePermission.test.tsx +++ b/plugins/permission-react/src/hooks/usePermission.test.tsx @@ -19,9 +19,8 @@ import { render } from '@testing-library/react'; import { usePermission } from './usePermission'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { TestApiProvider } from '@backstage/test-utils'; -import { permissionApiRef } from '../apis'; - -const mockAuthorize = jest.fn(); +import { PermissionApi, permissionApiRef } from '../apis'; +import { SWRConfig } from 'swr'; const permission = { name: 'access.something', @@ -39,49 +38,48 @@ const TestComponent: FC = () => { ); }; -describe('usePermission', () => { - it('Returns loading when permissionApi has not yet responded.', () => { - mockAuthorize.mockReturnValueOnce(new Promise(() => {})); - - const { getByText } = render( - +function renderComponent(mockApi: PermissionApi) { + return render( + new Map() }}> + - , - ); + + , + , + ); +} - expect(mockAuthorize).toHaveBeenCalledWith({ permission }); +describe('usePermission', () => { + const mockPermissionApi = { authorize: jest.fn() }; + + it('Returns loading when permissionApi has not yet responded.', () => { + mockPermissionApi.authorize.mockReturnValueOnce(new Promise(() => {})); + + const { getByText } = renderComponent(mockPermissionApi); + + expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); expect(getByText('loading')).toBeTruthy(); }); it('Returns allowed when permissionApi allows authorization.', async () => { - mockAuthorize.mockResolvedValueOnce({ result: AuthorizeResult.ALLOW }); + mockPermissionApi.authorize.mockResolvedValueOnce({ + result: AuthorizeResult.ALLOW, + }); - const { findByText } = render( - - - , - ); + const { findByText } = renderComponent(mockPermissionApi); - expect(mockAuthorize).toHaveBeenCalledWith({ permission }); + expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); expect(await findByText('content')).toBeTruthy(); }); it('Returns not allowed when permissionApi denies authorization.', async () => { - mockAuthorize.mockResolvedValueOnce({ result: AuthorizeResult.DENY }); + mockPermissionApi.authorize.mockResolvedValueOnce({ + result: AuthorizeResult.DENY, + }); - const { findByText } = render( - - - , - ); + const { findByText } = renderComponent(mockPermissionApi); - expect(mockAuthorize).toHaveBeenCalledWith({ permission }); + expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); await expect(findByText('content')).rejects.toThrowError(); }); }); diff --git a/plugins/permission-react/src/hooks/usePermission.ts b/plugins/permission-react/src/hooks/usePermission.ts index 45136d6697..cc27f82c59 100644 --- a/plugins/permission-react/src/hooks/usePermission.ts +++ b/plugins/permission-react/src/hooks/usePermission.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import useAsync from 'react-use/lib/useAsync'; import { useApi } from '@backstage/core-plugin-api'; import { permissionApiRef } from '../apis'; import { AuthorizeResult, Permission, } from '@backstage/plugin-permission-common'; +import useSWR from 'swr'; /** @public */ export type AsyncPermissionResult = { @@ -30,9 +30,16 @@ export type AsyncPermissionResult = { }; /** - * React hook utlity for authorization. Given a {@link @backstage/plugin-permission-common#Permission} and an optional - * resourceRef, it will return whether or not access is allowed (for the given resource, if resourceRef is provided). See - * {@link @backstage/plugin-permission-common/PermissionClient#authorize} for more details. + * React hook utility for authorization. Given a + * {@link @backstage/plugin-permission-common#Permission} and an optional + * resourceRef, it will return whether or not access is allowed (for the given + * resource, if resourceRef is provided). See + * {@link @backstage/plugin-permission-common/PermissionClient#authorize} for + * more details. + * + * Note: This hook uses stale-while-revalidate to help avoid flicker in UI + * elements that would be conditionally rendered based on the `allowed` result + * of this hook. * @public */ export const usePermission = ( @@ -40,21 +47,16 @@ export const usePermission = ( resourceRef?: string, ): AsyncPermissionResult => { const permissionApi = useApi(permissionApiRef); - - const { loading, error, value } = useAsync(async () => { - const { result } = await permissionApi.authorize({ - permission, - resourceRef, - }); - + const { data, error } = useSWR({ permission, resourceRef }, async args => { + const { result } = await permissionApi.authorize(args); return result; - }, [permissionApi, permission, resourceRef]); + }); - if (loading) { - return { loading: true, allowed: false }; - } if (error) { return { error, loading: false, allowed: false }; } - return { loading: false, allowed: value === AuthorizeResult.ALLOW }; + if (data === undefined) { + return { loading: true, allowed: false }; + } + return { loading: false, allowed: data === AuthorizeResult.ALLOW }; }; diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index d70862e1a2..7460eddd5a 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-proxy-backend +## 0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + +## 0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.2.17 + +### Patch Changes + +- 332d3decb2: Adds a new option `skipInvalidTargets` for the proxy `createRouter` which allows the proxy backend to be started with an invalid proxy configuration. If configured, it will simply skip the failed proxy and mount the other valid proxies. + + To configure it to pass by failing proxies: + + ``` + const router = await createRouter({ + config, + logger, + discovery, + skipInvalidProxies: true, + }); + ``` + + If you would like it to fail if a proxy is configured badly: + + ``` + const router = await createRouter({ + config, + logger, + discovery, + }); + ``` + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + +## 0.2.17-next.0 + +### Patch Changes + +- 332d3decb2: Adds a new option `skipInvalidTargets` for the proxy `createRouter` which allows the proxy backend to be started with an invalid proxy configuration. If configured, it will simply skip the failed proxy and mount the other valid proxies. + + To configure it to pass by failing proxies: + + ``` + const router = await createRouter({ + config, + logger, + discovery, + skipInvalidProxies: true, + }); + ``` + + If you would like it to fail if a proxy is configured badly: + + ``` + const router = await createRouter({ + config, + logger, + discovery, + }); + ``` + ## 0.2.16 ### Patch Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 78e6a2e553..5336bbb098 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-proxy-backend", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", - "version": "0.2.16", + "version": "0.2.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config": "^0.1.13", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -43,7 +43,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/proxy-backend/src/service/router.test.ts b/plugins/proxy-backend/src/service/router.test.ts index ddb524d3aa..3bfcc889f4 100644 --- a/plugins/proxy-backend/src/service/router.test.ts +++ b/plugins/proxy-backend/src/service/router.test.ts @@ -30,23 +30,96 @@ const mockCreateProxyMiddleware = createProxyMiddleware as jest.MockedFunction< >; describe('createRouter', () => { - it('works', async () => { - const logger = getVoidLogger(); - const config = new ConfigReader({ - backend: { - baseUrl: 'https://example.com:7007', - listen: { - port: 7007, + describe('where all proxy config are valid', () => { + it('works', async () => { + const logger = getVoidLogger(); + const config = new ConfigReader({ + backend: { + baseUrl: 'https://example.com:7007', + listen: { + port: 7007, + }, }, - }, + proxy: { + '/test': { + target: 'https://example.com', + headers: { + Authorization: 'Bearer supersecret', + }, + }, + }, + }); + const discovery = SingleHostDiscovery.fromConfig(config); + const router = await createRouter({ + config, + logger, + discovery, + }); + expect(router).toBeDefined(); }); - const discovery = SingleHostDiscovery.fromConfig(config); - const router = await createRouter({ - config, - logger, - discovery, + }); + + describe('where buildMiddleware would fail', () => { + it('throws an error if skip failures is not set', async () => { + const logger = getVoidLogger(); + logger.warn = jest.fn(); + const config = new ConfigReader({ + backend: { + baseUrl: 'https://example.com:7007', + listen: { + port: 7007, + }, + }, + // no target would cause the buildMiddleware to fail + proxy: { + '/test': { + headers: { + Authorization: 'Bearer supersecret', + }, + }, + }, + }); + const discovery = SingleHostDiscovery.fromConfig(config); + await expect( + createRouter({ + config, + logger, + discovery, + }), + ).rejects.toThrow(new Error('Proxy target must be a string')); + }); + + it('works if skip failures is set', async () => { + const logger = getVoidLogger(); + logger.warn = jest.fn(); + const config = new ConfigReader({ + backend: { + baseUrl: 'https://example.com:7007', + listen: { + port: 7007, + }, + }, + // no target would cause the buildMiddleware to fail + proxy: { + '/test': { + headers: { + Authorization: 'Bearer supersecret', + }, + }, + }, + }); + const discovery = SingleHostDiscovery.fromConfig(config); + const router = await createRouter({ + config, + logger, + discovery, + skipInvalidProxies: true, + }); + expect((logger.warn as jest.Mock).mock.calls[0][0]).toEqual( + 'skipped configuring /test due to Proxy target must be a string', + ); + expect(router).toBeDefined(); }); - expect(router).toBeDefined(); }); }); diff --git a/plugins/proxy-backend/src/service/router.ts b/plugins/proxy-backend/src/service/router.ts index c2123ae4b1..ca2c7870d8 100644 --- a/plugins/proxy-backend/src/service/router.ts +++ b/plugins/proxy-backend/src/service/router.ts @@ -51,6 +51,7 @@ export interface RouterOptions { logger: Logger; config: Config; discovery: PluginEndpointDiscovery; + skipInvalidProxies?: boolean; } export interface ProxyConfig extends Options { @@ -185,11 +186,20 @@ export async function createRouter( const { pathname: pathPrefix } = new URL(externalUrl); const proxyConfig = options.config.getOptional('proxy') ?? {}; + Object.entries(proxyConfig).forEach(([route, proxyRouteConfig]) => { - router.use( - route, - buildMiddleware(pathPrefix, options.logger, route, proxyRouteConfig), - ); + try { + router.use( + route, + buildMiddleware(pathPrefix, options.logger, route, proxyRouteConfig), + ); + } catch (e) { + if (options.skipInvalidProxies) { + options.logger.warn(`skipped configuring ${route} due to ${e.message}`); + } else { + throw e; + } + } }); return router; diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index a8feeee4a6..14e566219a 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-rollbar-backend +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.1.20 + +### Patch Changes + +- 91faf87aaf: chore(deps): bump `camelcase-keys` from 6.2.2 to 7.0.1 +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + +## 0.1.20-next.0 + +### Patch Changes + +- 91faf87aaf: chore(deps): bump `camelcase-keys` from 6.2.2 to 7.0.1 + ## 0.1.19 ### Patch Changes diff --git a/plugins/rollbar-backend/api-report.md b/plugins/rollbar-backend/api-report.md index 0fa4383564..d51292121f 100644 --- a/plugins/rollbar-backend/api-report.md +++ b/plugins/rollbar-backend/api-report.md @@ -33,13 +33,21 @@ export class RollbarApi { environment: string; item_id?: number; }, - ): Promise; - // Warning: (ae-forgotten-export) The symbol "RollbarProject" needs to be exported by the entry point index.d.ts - // + ): Promise< + { + count: number; + timestamp: number; + }[] + >; // (undocumented) - getAllProjects(): Promise; - // Warning: (ae-forgotten-export) The symbol "RollbarItemCount" needs to be exported by the entry point index.d.ts - // + getAllProjects(): Promise< + { + id: number; + name: string; + status: string; + accountId: number; + }[] + >; // (undocumented) getOccuranceCounts( projectName: string, @@ -47,15 +55,25 @@ export class RollbarApi { environment: string; item_id?: number; }, - ): Promise; + ): Promise< + { + count: number; + timestamp: number; + }[] + >; // (undocumented) - getProject(projectName: string): Promise; - // Warning: (ae-forgotten-export) The symbol "RollbarItemsResponse" needs to be exported by the entry point index.d.ts - // + getProject(projectName: string): Promise<{ + id: number; + name: string; + status: string; + accountId: number; + }>; // (undocumented) - getProjectItems(projectName: string): Promise; - // Warning: (ae-forgotten-export) The symbol "RollbarTopActiveItem" needs to be exported by the entry point index.d.ts - // + getProjectItems(projectName: string): Promise<{ + page: number; + items: RollbarItem[]; + totalCount: number; + }>; // (undocumented) getTopActiveItems( projectName: string, @@ -63,7 +81,23 @@ export class RollbarApi { hours: number; environment: string; }, - ): Promise; + ): Promise< + { + item: { + id: number; + counter: number; + environment: string; + framework: RollbarFrameworkId; + lastOccurrenceTimestamp: number; + level: number; + occurrences: number; + projectId: number; + title: string; + uniqueOccurrences: number; + }; + counts: number[]; + }[] + >; } // Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -77,4 +111,9 @@ export interface RouterOptions { // (undocumented) rollbarApi?: RollbarApi; } + +// Warnings were encountered during analysis: +// +// src/api/RollbarApi.d.ts:21:9 - (ae-forgotten-export) The symbol "RollbarItem" needs to be exported by the entry point index.d.ts +// src/api/RollbarApi.d.ts:32:13 - (ae-forgotten-export) The symbol "RollbarFrameworkId" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index c13d16f63b..36683ef66a 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar-backend", "description": "A Backstage backend plugin that integrates towards Rollbar", - "version": "0.1.19", + "version": "0.1.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config": "^0.1.13", "@types/express": "^4.17.6", - "camelcase-keys": "^6.2.2", + "camelcase-keys": "^7.0.1", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", @@ -43,13 +43,13 @@ "helmet": "^4.0.0", "lodash": "^4.17.21", "morgan": "^1.10.0", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/test-utils": "^0.2.4", "@types/supertest": "^2.0.8", "msw": "^0.36.3", "supertest": "^6.1.3" diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index d414e3d841..df1cb19f83 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-rollbar +## 0.3.26 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.3.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.3.25 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.3.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.3.24 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index dc7ede62cf..5767f26c5e 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.3.24", + "version": "0.3.26", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,9 +33,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -50,10 +50,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -61,7 +61,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/react": "^16.13.1 || ^17.0.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index fd0bf3e177..29ff4e9edb 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + - @backstage/plugin-scaffolder-backend@0.15.24 + +## 0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + - @backstage/plugin-scaffolder-backend@0.15.24-next.0 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.23 + - @backstage/backend-common@0.10.6 + +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + - @backstage/plugin-scaffolder-backend@0.15.23-next.1 + +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.23-next.0 + ## 0.1.9 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index e1571c95b1..cee138b576 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.1.9", + "version": "0.1.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/errors": "^0.2.0", "@backstage/integration": "^0.7.2", - "@backstage/plugin-scaffolder-backend": "^0.15.22", + "@backstage/plugin-scaffolder-backend": "^0.15.24", "@backstage/config": "^0.1.13", "@backstage/types": "^0.1.1", "command-exists": "^1.2.9", @@ -32,7 +32,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/jest": "^26.0.7", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 4615185fb4..2b369835f8 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + - @backstage/plugin-scaffolder-backend@0.15.24 + +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + - @backstage/plugin-scaffolder-backend@0.15.24-next.0 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.23 + - @backstage/backend-common@0.10.6 + +## 0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + - @backstage/plugin-scaffolder-backend@0.15.23-next.1 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.23-next.0 + ## 0.2.4 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 2779b42631..5f32dbadd8 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.2.4", + "version": "0.2.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/plugin-scaffolder-backend": "^0.15.22", + "@backstage/backend-common": "^0.10.7", + "@backstage/plugin-scaffolder-backend": "^0.15.24", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", "@backstage/integration": "^0.7.2", @@ -31,7 +31,7 @@ "fs-extra": "^9.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/command-exists": "^1.2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 02c2a7f7bd..8c5a6ade44 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.24 + +## 0.1.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.24-next.0 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.23 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@0.15.23-next.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index fe2e7dfc6a..b2ce1bf571 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.1.3", + "version": "0.1.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,13 +21,13 @@ }, "dependencies": { "@backstage/config": "^0.1.13", - "@backstage/plugin-scaffolder-backend": "^0.15.21", + "@backstage/plugin-scaffolder-backend": "^0.15.24", "@backstage/types": "^0.1.1", "winston": "^3.2.1", - "yeoman-environment": "^3.6.0" + "yeoman-environment": "^3.9.1" }, "devDependencies": { - "@backstage/backend-common": "^0.10.4", + "@backstage/backend-common": "^0.10.7", "@types/jest": "^26.0.7" }, "files": [ diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 9941990066..7c80464d71 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-scaffolder-backend +## 0.15.24 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- 2bd5f24043: fix for the `gitlab:publish` action to use the `oauthToken` key when creating a + `Gitlab` client. This only happens if `ctx.input.token` is provided else the key `token` will be used. +- 898a56578c: Bump `vm2` to version 3.9.6 +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/backend-common@0.10.7 + - @backstage/plugin-catalog-backend@0.21.3 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.11 + +## 0.15.24-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- 2bd5f24043: fix for the `gitlab:publish` action to use the `oauthToken` key when creating a + `Gitlab` client. This only happens if `ctx.input.token` is provided else the key `token` will be used. +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + - @backstage/plugin-catalog-backend@0.21.3-next.0 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.11-next.0 + +## 0.15.23 + +### Patch Changes + +- 2e0dbb0e50: Migrate from deprecated package @octokit/rest to octokit +- c95df1631e: Added support for templating secrets into actions input, and also added an extra `token` input argument to all publishers to provide a token that would override the `integrations.config`. + You can find more information over at [Writing Templates](https://backstage.io/docs/features/software-templates/writing-templates#using-the-users-oauth-token) +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.2 + - @backstage/backend-common@0.10.6 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.10 + +## 0.15.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + - @backstage/plugin-catalog-backend@0.21.2-next.1 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.10-next.1 + +## 0.15.23-next.0 + +### Patch Changes + +- 2e0dbb0e50: Migrate from deprecated package @octokit/rest to octokit +- Updated dependencies + - @backstage/plugin-catalog-backend@0.21.2-next.0 + - @backstage/plugin-scaffolder-backend-module-cookiecutter@0.1.10-next.0 + ## 0.15.22 ### Patch Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index c33aa91bee..5e7b5a428b 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -20,7 +20,7 @@ import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; -import { Octokit } from '@octokit/rest'; +import { Octokit } from 'octokit'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { Schema } from 'jsonschema'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -299,7 +299,12 @@ export class OctokitProvider { githubCredentialsProvider?: GithubCredentialsProvider, ); // Warning: (ae-forgotten-export) The symbol "OctokitIntegration" needs to be exported by the entry point index.d.ts - getOctokit(repoUrl: string): Promise; + getOctokit( + repoUrl: string, + options?: { + token?: string; + }, + ): Promise; } // @public diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 2055a3b90d..3161448562 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "0.15.22", + "version": "0.15.24", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,19 +31,18 @@ "build:assets": "node scripts/build-nunjucks.js" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/catalog-client": "^0.5.5", + "@backstage/backend-common": "^0.10.7", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", "@backstage/integration": "^0.7.2", - "@backstage/plugin-catalog-backend": "^0.21.1", + "@backstage/plugin-catalog-backend": "^0.21.3", "@backstage/plugin-scaffolder-common": "^0.1.3", - "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.9", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.11", "@backstage/types": "^0.1.1", "@gitbeaker/core": "^34.6.0", "@gitbeaker/node": "^35.1.0", - "@octokit/rest": "^18.5.3", "@octokit/webhooks": "^9.14.1", "@types/express": "^4.17.6", "azure-devops-node-api": "^11.0.1", @@ -60,21 +59,22 @@ "isbinaryfile": "^4.0.8", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", - "knex": "^0.95.1", + "knex": "^1.0.2", "lodash": "^4.17.21", "luxon": "^2.0.2", "morgan": "^1.10.0", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "nunjucks": "^3.2.3", + "octokit": "^1.7.1", "octokit-plugin-create-pull-request": "^3.10.0", "uuid": "^8.2.0", "winston": "^3.2.1", "yaml": "^1.10.0", - "vm2": "^3.9.5" + "vm2": "^3.9.6" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/test-utils": "^0.2.4", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts b/plugins/scaffolder-backend/src/scaffolder/__mocks__/octokit/index.ts similarity index 71% rename from plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts rename to plugins/scaffolder-backend/src/scaffolder/__mocks__/octokit/index.ts index b60c3fcb2d..de320ef498 100644 --- a/plugins/scaffolder-backend/src/scaffolder/__mocks__/@octokit/rest/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/__mocks__/octokit/index.ts @@ -19,19 +19,19 @@ export const mockGithubClient = { actions: { createWorkflowDispatch: jest.fn(), }, - }, - repos: { - createInOrg: jest.fn(), - createForAuthenticatedUser: jest.fn(), - createWebhook: jest.fn(), - addCollaborator: jest.fn(), - replaceAllTopics: jest.fn(), - }, - users: { - getByUsername: jest.fn(), - }, - teams: { - addOrUpdateRepoPermissionsInOrg: jest.fn(), + repos: { + createInOrg: jest.fn(), + createForAuthenticatedUser: jest.fn(), + createWebhook: jest.fn(), + addCollaborator: jest.fn(), + replaceAllTopics: jest.fn(), + }, + users: { + getByUsername: jest.fn(), + }, + teams: { + addOrUpdateRepoPermissionsInOrg: jest.fn(), + }, }, }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts index 9612780b12..dd6f29f99b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts @@ -23,7 +23,7 @@ const fsMock = fs as jest.Mocked; import { PassThrough } from 'stream'; import os from 'os'; import { getVoidLogger } from '@backstage/backend-common'; -import { ORIGIN_LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import { ANNOTATION_ORIGIN_LOCATION } from '@backstage/catalog-model'; import { createCatalogWriteAction } from './write'; import { resolve as resolvePath } from 'path'; import * as yaml from 'yaml'; @@ -51,7 +51,7 @@ describe('catalog:write', () => { name: 'n', namespace: 'ns', annotations: { - [ORIGIN_LOCATION_ANNOTATION]: 'url:https://example.com', + [ANNOTATION_ORIGIN_LOCATION]: 'url:https://example.com', }, }, spec: {}, @@ -79,7 +79,7 @@ describe('catalog:write', () => { name: 'n', namespace: 'ns', annotations: { - [ORIGIN_LOCATION_ANNOTATION]: 'url:https://example.com', + [ANNOTATION_ORIGIN_LOCATION]: 'url:https://example.com', }, }, spec: {}, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index cf1662307c..6ee8f8ce01 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -35,7 +35,6 @@ jest.mock('./helpers', () => ({ const realFiles = Object.fromEntries( [ - require.resolve('vm2/lib/fixasync'), resolvePackagePath( '@backstage/plugin-scaffolder-backend', 'assets', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts index 0d4353c857..e2449917ab 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.test.ts @@ -74,4 +74,15 @@ describe('getOctokit', () => { expect(owner).toBe('owner'); expect(repo).toBe('bob'); }); + + it('should return an octokit client with the passed in token if it is provided', async () => { + const { client, token, owner, repo } = await octokitProvider.getOctokit( + 'github.com?repo=bob&owner=owner', + { token: 'tokenlols2' }, + ); + expect(client).toBeDefined(); + expect(token).toBe('tokenlols2'); + expect(owner).toBe('owner'); + expect(repo).toBe('bob'); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts index bf91c0e37c..3ac66a0023 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/OctokitProvider.ts @@ -20,7 +20,7 @@ import { GithubCredentialsProvider, ScmIntegrationRegistry, } from '@backstage/integration'; -import { Octokit } from '@octokit/rest'; +import { Octokit } from 'octokit'; import { parseRepoUrl } from '../publish/util'; export type OctokitIntegration = { @@ -52,7 +52,10 @@ export class OctokitProvider { * * @param repoUrl - Repository URL */ - async getOctokit(repoUrl: string): Promise { + async getOctokit( + repoUrl: string, + options?: { token?: string }, + ): Promise { const { owner, repo, host } = parseRepoUrl(repoUrl, this.integrations); if (!owner) { @@ -65,7 +68,19 @@ export class OctokitProvider { throw new InputError(`No integration for host ${host}`); } - // TODO(blam): Consider changing this API to have owner, repo interface instead of URL as the it's + // Short circuit the internal Github Token provider the token provided + // by the action or the caller. + if (options?.token) { + const client = new Octokit({ + auth: options.token, + baseUrl: integrationConfig.apiBaseUrl, + previews: ['nebula-preview'], + }); + + return { client, token: options.token, owner, repo }; + } + + // TODO(blam): Consider changing this API to have owner, repoo interface instead of URL as the it's // needless to create URL and then parse again the other side. const { token } = await this.githubCredentialsProvider.getCredentials({ url: `https://${host}/${encodeURIComponent(owner)}/${encodeURIComponent( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts index 6cdb2ee58c..7850163e9d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -jest.mock('@octokit/rest'); +jest.mock('octokit'); import { TemplateAction } from '../../types'; import { createGithubActionsDispatchAction } from './githubActionsDispatch'; @@ -54,7 +54,7 @@ describe('github:actions:dispatch', () => { createTemporaryDirectory: jest.fn(), }; - const { mockGithubClient } = require('@octokit/rest'); + const { mockGithubClient } = require('octokit'); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts index 1f7f0f60b9..0c8b4d4aab 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubActionsDispatch.ts @@ -37,6 +37,7 @@ export function createGithubActionsDispatchAction(options: { workflowId: string; branchOrTagName: string; workflowInputs?: { [key: string]: string }; + token?: string; }>({ id: 'github:actions:dispatch', description: @@ -68,18 +69,31 @@ export function createGithubActionsDispatchAction(options: { 'Inputs keys and values to send to GitHub Action configured on the workflow file. The maximum number of properties is 10. ', type: 'object', }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The GITHUB_TOKEN to use for authorization to GitHub', + }, }, }, }, async handler(ctx) { - const { repoUrl, workflowId, branchOrTagName, workflowInputs } = - ctx.input; + const { + repoUrl, + workflowId, + branchOrTagName, + workflowInputs, + token: providedToken, + } = ctx.input; ctx.logger.info( `Dispatching workflow ${workflowId} for repo ${repoUrl} on ${branchOrTagName}`, ); - const { client, owner, repo } = await octokitProvider.getOctokit(repoUrl); + const { client, owner, repo } = await octokitProvider.getOctokit( + repoUrl, + { token: providedToken }, + ); await client.rest.actions.createWorkflowDispatch({ owner, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts index fe2535ca42..b80691fdcc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -jest.mock('@octokit/rest'); +jest.mock('octokit'); import { createGithubWebhookAction } from './githubWebhook'; import { @@ -65,7 +65,7 @@ describe('github:repository:webhook:create', () => { createTemporaryDirectory: jest.fn(), }; - const { mockGithubClient } = require('@octokit/rest'); + const { mockGithubClient } = require('octokit'); it('should call the githubApi for creating repository Webhook', async () => { const repoUrl = 'github.com?repo=repo&owner=owner'; @@ -75,7 +75,7 @@ describe('github:repository:webhook:create', () => { }); await action.handler(ctx); - expect(mockGithubClient.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], @@ -97,7 +97,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], @@ -118,7 +118,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push', 'pull_request'], @@ -139,7 +139,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], @@ -160,7 +160,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], @@ -181,7 +181,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], @@ -202,7 +202,7 @@ describe('github:repository:webhook:create', () => { }, }); - expect(mockGithubClient.repos.createWebhook).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.createWebhook).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', events: ['push'], diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts index 95f1888882..660c7793f2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubWebhook.ts @@ -47,6 +47,7 @@ export function createGithubWebhookAction(options: { active?: boolean; contentType?: ContentType; insecureSsl?: boolean; + token?: string; }>({ id: 'github:webhook', description: 'Creates webhook for a repository on GitHub.', @@ -107,6 +108,11 @@ export function createGithubWebhookAction(options: { type: 'boolean', description: `Determines whether the SSL certificate of the host for url will be verified when delivering payloads. Default 'false'`, }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The GITHUB_TOKEN to use for authorization to GitHub', + }, }, }, }, @@ -119,15 +125,19 @@ export function createGithubWebhookAction(options: { active = true, contentType = 'form', insecureSsl = false, + token: providedToken, } = ctx.input; ctx.logger.info(`Creating webhook ${webhookUrl} for repo ${repoUrl}`); - const { client, owner, repo } = await octokitProvider.getOctokit(repoUrl); + const { client, owner, repo } = await octokitProvider.getOctokit( + repoUrl, + { token: providedToken }, + ); try { const insecure_ssl = insecureSsl ? '1' : '0'; - await client.repos.createWebhook({ + await client.rest.repos.createWebhook({ owner, repo, config: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 90f060fd88..9303b5e487 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -18,7 +18,7 @@ import { SpawnOptionsWithoutStdio, spawn } from 'child_process'; import { PassThrough, Writable } from 'stream'; import { Logger } from 'winston'; import { Git } from '@backstage/backend-common'; -import { Octokit } from '@octokit/rest'; +import { Octokit } from 'octokit'; import { assertError } from '@backstage/errors'; export type RunCommandOptions = { @@ -58,7 +58,9 @@ export const runCommand = async ({ process.on('close', code => { if (code !== 0) { - return reject(`Command ${command} failed, exit code: ${code}`); + return reject( + new Error(`Command ${command} failed, exit code: ${code}`), + ); } return resolve(); }); @@ -139,7 +141,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ }: BranchProtectionOptions): Promise => { const tryOnce = async () => { try { - await client.repos.updateBranchProtection({ + await client.rest.repos.updateBranchProtection({ mediaType: { /** * 👇 we need this preview because allowing a custom 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 cee99e1646..9247109121 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 @@ -119,6 +119,32 @@ describe('publish:azure', () => { ).rejects.toThrow(/Unable to create the repository/); }); + it('should not throw if there is a token provided through ctx.input', async () => { + mockGitClient.createRepository.mockImplementation(() => ({ + remoteUrl: 'http://google.com', + })); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'myazurehostnotoken.com?repo=bob&owner=owner&organization=org', + token: 'lols', + }, + }); + + expect(WebApi).toHaveBeenCalledWith( + 'https://myazurehostnotoken.com/org', + expect.any(Function), + ); + + expect(mockGitClient.createRepository).toHaveBeenCalledWith( + { + name: 'bob', + }, + 'owner', + ); + }); + it('should throw if there is no remoteUrl returned', async () => { mockGitClient.createRepository.mockImplementation(() => ({ remoteUrl: null, 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 7e394a8d8b..4f67a0e45f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/azure.ts @@ -34,6 +34,7 @@ export function createPublishAzureAction(options: { description?: string; defaultBranch?: string; sourcePath?: string; + token?: string; }>({ id: 'publish:azure', description: @@ -57,10 +58,16 @@ export function createPublishAzureAction(options: { description: `Sets the default branch on the repository. The default value is 'master'`, }, sourcePath: { - title: + title: 'Source Path', + description: 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', type: 'string', }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to Azure', + }, }, }, output: { @@ -98,12 +105,13 @@ export function createPublishAzureAction(options: { `No matching integration configuration for host ${host}, please check your integrations config`, ); } - if (!integrationConfig.config.token) { + + if (!integrationConfig.config.token && !ctx.input.token) { throw new InputError(`No token provided for Azure Integration ${host}`); } - const authHandler = getPersonalAccessTokenHandler( - integrationConfig.config.token, - ); + + const token = ctx.input.token ?? integrationConfig.config.token!; + const authHandler = getPersonalAccessTokenHandler(token); const webApi = new WebApi(`https://${host}/${organization}`, authHandler); const client = await webApi.getGitApi(); @@ -139,7 +147,7 @@ export function createPublishAzureAction(options: { defaultBranch, auth: { username: 'notempty', - password: integrationConfig.config.token, + password: token, }, logger: ctx.logger, commitMessage: config.getOptionalString( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts index 2678abd9dd..3d14710f3e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.test.ts @@ -194,6 +194,45 @@ describe('publish:bitbucket', () => { }); }); + it('should work if the token is provided through ctx.input', async () => { + expect.assertions(2); + server.use( + rest.post( + 'https://notoken.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer lols'); + expect(req.body).toEqual({ public: false, name: 'repo' }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ); + await action.handler({ + ...mockContext, + input: { + repoUrl: 'notoken.bitbucket.com?project=project&repo=repo', + token: 'lols', + }, + }); + }); + describe('LFS for hosted bitbucket', () => { const repoCreationResponse = { links: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts index fbfe87b076..8ba8592642 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/bitbucket.ts @@ -205,6 +205,7 @@ export function createPublishBitbucketAction(options: { repoVisibility: 'private' | 'public'; sourcePath?: string; enableLFS: boolean; + token?: string; }>({ id: 'publish:bitbucket', description: @@ -233,15 +234,22 @@ export function createPublishBitbucketAction(options: { description: `Sets the default branch on the repository. The default value is 'master'`, }, sourcePath: { - title: + title: 'Source Path', + description: 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', type: 'string', }, enableLFS: { - title: + title: 'Enable LFS?', + description: 'Enable LFS for the repository. Only available for hosted Bitbucket.', type: 'boolean', }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to BitBucket', + }, }, }, output: { @@ -296,7 +304,12 @@ export function createPublishBitbucketAction(options: { ); } - const authorization = getAuthorizationHeader(integrationConfig.config); + const authorization = getAuthorizationHeader( + ctx.input.token + ? { host: integrationConfig.config.host, token: ctx.input.token } + : integrationConfig.config, + ); + const apiBaseUrl = integrationConfig.config.apiBaseUrl; const createMethod = @@ -320,17 +333,28 @@ export function createPublishBitbucketAction(options: { email: config.getOptionalString('scaffolder.defaultAuthor.email'), }; - await initRepoAndPush({ - dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), - remoteUrl, - auth: { + let auth; + + if (ctx.input.token) { + auth = { + username: 'x-token-auth', + password: ctx.input.token, + }; + } else { + auth = { username: integrationConfig.config.username ? integrationConfig.config.username : 'x-token-auth', password: integrationConfig.config.appPassword ? integrationConfig.config.appPassword : integrationConfig.config.token ?? '', - }, + }; + } + + await initRepoAndPush({ + dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath), + remoteUrl, + auth, defaultBranch, logger: ctx.logger, commitMessage: config.getOptionalString( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 7724dec85e..5467ca651c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -17,7 +17,7 @@ import { TemplateAction } from '../../types'; jest.mock('../helpers'); -jest.mock('@octokit/rest'); +jest.mock('octokit'); import { createPublishGithubAction } from './github'; import { @@ -62,7 +62,7 @@ describe('publish:github', () => { createTemporaryDirectory: jest.fn(), }; - const { mockGithubClient } = require('@octokit/rest'); + const { mockGithubClient } = require('octokit'); beforeEach(() => { jest.resetAllMocks(); @@ -76,14 +76,14 @@ describe('publish:github', () => { }); it('should call the githubApis with the correct values for createInOrg', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'Organization' }, }); - mockGithubClient.repos.createInOrg.mockResolvedValue({ data: {} }); + mockGithubClient.rest.repos.createInOrg.mockResolvedValue({ data: {} }); await action.handler(mockContext); - expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.createInOrg).toHaveBeenCalledWith({ description: 'description', name: 'repo', org: 'owner', @@ -98,7 +98,7 @@ describe('publish:github', () => { repoVisibility: 'public', }, }); - expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.createInOrg).toHaveBeenCalledWith({ description: 'description', name: 'repo', org: 'owner', @@ -108,17 +108,17 @@ describe('publish:github', () => { }); it('should call the githubApis with the correct values for createForAuthenticatedUser', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: {}, }); await action.handler(mockContext); expect( - mockGithubClient.repos.createForAuthenticatedUser, + mockGithubClient.rest.repos.createForAuthenticatedUser, ).toHaveBeenCalledWith({ description: 'description', name: 'repo', @@ -133,7 +133,7 @@ describe('publish:github', () => { }, }); expect( - mockGithubClient.repos.createForAuthenticatedUser, + mockGithubClient.rest.repos.createForAuthenticatedUser, ).toHaveBeenCalledWith({ description: 'description', name: 'repo', @@ -142,11 +142,11 @@ describe('publish:github', () => { }); it('should call initRepoAndPush with the correct values', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -166,11 +166,11 @@ describe('publish:github', () => { }); it('should call initRepoAndPush with the correct defaultBranch main', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -219,11 +219,11 @@ describe('publish:github', () => { githubCredentialsProvider, }); - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -263,11 +263,11 @@ describe('publish:github', () => { githubCredentialsProvider, }); - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -288,11 +288,11 @@ describe('publish:github', () => { }); it('should add access for the team when it starts with the owner', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -302,7 +302,7 @@ describe('publish:github', () => { await action.handler(mockContext); expect( - mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg, + mockGithubClient.rest.teams.addOrUpdateRepoPermissionsInOrg, ).toHaveBeenCalledWith({ org: 'owner', team_slug: 'blam', @@ -313,11 +313,11 @@ describe('publish:github', () => { }); it('should add outside collaborators when provided', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -332,7 +332,7 @@ describe('publish:github', () => { }, }); - expect(mockGithubClient.repos.addCollaborator).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.addCollaborator).toHaveBeenCalledWith({ username: 'outsidecollaborator', owner: 'owner', repo: 'repo', @@ -341,11 +341,11 @@ describe('publish:github', () => { }); it('should add multiple collaborators when provided', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -376,7 +376,7 @@ describe('publish:github', () => { }; expect( - mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[1], + mockGithubClient.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[1], ).toEqual([ { ...commonProperties, @@ -386,7 +386,7 @@ describe('publish:github', () => { ]); expect( - mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2], + mockGithubClient.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2], ).toEqual([ { ...commonProperties, @@ -397,18 +397,18 @@ describe('publish:github', () => { }); it('should ignore failures when adding multiple collaborators', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', }, }); - when(mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg) + when(mockGithubClient.rest.teams.addOrUpdateRepoPermissionsInOrg) .calledWith({ org: 'owner', owner: 'owner', @@ -436,7 +436,7 @@ describe('publish:github', () => { }); expect( - mockGithubClient.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2], + mockGithubClient.rest.teams.addOrUpdateRepoPermissionsInOrg.mock.calls[2], ).toEqual([ { org: 'owner', @@ -449,18 +449,18 @@ describe('publish:github', () => { }); it('should add topics when provided', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', }, }); - mockGithubClient.repos.replaceAllTopics.mockResolvedValue({ + mockGithubClient.rest.repos.replaceAllTopics.mockResolvedValue({ data: { names: ['node.js'], }, @@ -474,7 +474,7 @@ describe('publish:github', () => { }, }); - expect(mockGithubClient.repos.replaceAllTopics).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', names: ['node.js'], @@ -482,18 +482,18 @@ describe('publish:github', () => { }); it('should lowercase topics when provided', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', }, }); - mockGithubClient.repos.replaceAllTopics.mockResolvedValue({ + mockGithubClient.rest.repos.replaceAllTopics.mockResolvedValue({ data: { names: ['backstage'], }, @@ -507,7 +507,7 @@ describe('publish:github', () => { }, }); - expect(mockGithubClient.repos.replaceAllTopics).toHaveBeenCalledWith({ + expect(mockGithubClient.rest.repos.replaceAllTopics).toHaveBeenCalledWith({ owner: 'owner', repo: 'repo', names: ['backstage'], @@ -515,11 +515,11 @@ describe('publish:github', () => { }); it('should call output with the remoteUrl and the repoContentsUrl', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -539,11 +539,11 @@ describe('publish:github', () => { }); it('should use main as default branch', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { clone_url: 'https://github.com/clone/url.git', html_url: 'https://github.com/html/url', @@ -569,11 +569,11 @@ describe('publish:github', () => { }); it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requireCodeOwnerReviews', async () => { - mockGithubClient.users.getByUsername.mockResolvedValue({ + mockGithubClient.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, }); - mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + mockGithubClient.rest.repos.createForAuthenticatedUser.mockResolvedValue({ data: { name: 'repository', }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index 3e529b1f17..139fad04d4 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -52,6 +52,7 @@ export function createPublishGithubAction(options: { requireCodeOwnerReviews?: boolean; repoVisibility: 'private' | 'internal' | 'public'; collaborators: Collaborator[]; + token?: string; topics?: string[]; }>({ id: 'publish:github', @@ -77,7 +78,8 @@ export function createPublishGithubAction(options: { type: 'string', }, requireCodeOwnerReviews: { - title: + title: 'Require CODEOWNER Reviews?', + description: 'Require an approved review in PR including files with a designated Code Owner', type: 'boolean', }, @@ -92,7 +94,8 @@ export function createPublishGithubAction(options: { description: `Sets the default branch on the repository. The default value is 'master'`, }, sourcePath: { - title: + title: 'Source Path', + description: 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', type: 'string', }, @@ -116,6 +119,11 @@ export function createPublishGithubAction(options: { }, }, }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitHub', + }, topics: { title: 'Topics', type: 'array', @@ -149,26 +157,28 @@ export function createPublishGithubAction(options: { defaultBranch = 'master', collaborators, topics, + token: providedToken, } = ctx.input; const { client, token, owner, repo } = await octokitProvider.getOctokit( repoUrl, + { token: providedToken }, ); - const user = await client.users.getByUsername({ + const user = await client.rest.users.getByUsername({ username: owner, }); const repoCreationPromise = user.data.type === 'Organization' - ? client.repos.createInOrg({ + ? client.rest.repos.createInOrg({ name: repo, org: owner, private: repoVisibility === 'private', visibility: repoVisibility, description: description, }) - : client.repos.createForAuthenticatedUser({ + : client.rest.repos.createForAuthenticatedUser({ name: repo, private: repoVisibility === 'private', description: description, @@ -177,7 +187,7 @@ export function createPublishGithubAction(options: { const { data: newRepo } = await repoCreationPromise; if (access?.startsWith(`${owner}/`)) { const [, team] = access.split('/'); - await client.teams.addOrUpdateRepoPermissionsInOrg({ + await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ org: owner, team_slug: team, owner, @@ -186,7 +196,7 @@ export function createPublishGithubAction(options: { }); // No need to add access if it's the person who owns the personal account } else if (access && access !== owner) { - await client.repos.addCollaborator({ + await client.rest.repos.addCollaborator({ owner, repo, username: access, @@ -200,7 +210,7 @@ export function createPublishGithubAction(options: { username: team_slug, } of collaborators) { try { - await client.teams.addOrUpdateRepoPermissionsInOrg({ + await client.rest.teams.addOrUpdateRepoPermissionsInOrg({ org: owner, team_slug, owner, @@ -218,7 +228,7 @@ export function createPublishGithubAction(options: { if (topics) { try { - await client.repos.replaceAllTopics({ + await client.rest.repos.replaceAllTopics({ owner, repo, names: topics.map(t => t.toLowerCase()), diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts index 7b3e89ac2a..80333c4966 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.ts @@ -24,7 +24,7 @@ import { } from '@backstage/integration'; import { zipObject } from 'lodash'; import { createTemplateAction } from '../../createTemplateAction'; -import { Octokit } from '@octokit/rest'; +import { Octokit } from 'octokit'; import { InputError, CustomErrorBase } from '@backstage/errors'; import { createPullRequest } from 'octokit-plugin-create-pull-request'; import globby from 'globby'; @@ -55,6 +55,7 @@ export type GithubPullRequestActionInput = { repoUrl: string; targetPath?: string; sourcePath?: string; + token?: string; }; export type ClientFactoryInput = { @@ -63,6 +64,7 @@ export type ClientFactoryInput = { host: string; owner: string; repo: string; + token?: string; }; export const defaultClientFactory = async ({ @@ -71,13 +73,22 @@ export const defaultClientFactory = async ({ owner, repo, host = 'github.com', + token: providedToken, }: ClientFactoryInput): Promise => { const integrationConfig = integrations.github.byHost(host)?.config; + const OctokitPR = Octokit.plugin(createPullRequest); if (!integrationConfig) { throw new InputError(`No integration for host ${host}`); } + if (providedToken) { + return new OctokitPR({ + auth: providedToken, + baseUrl: integrationConfig.apiBaseUrl, + }); + } + const credentialsProvider = githubCredentialsProvider || SingleInstanceGithubCredentialsProvider.create(integrationConfig); @@ -94,8 +105,6 @@ export const defaultClientFactory = async ({ ); } - const OctokitPR = Octokit.plugin(createPullRequest); - return new OctokitPR({ auth: token, baseUrl: integrationConfig.apiBaseUrl, @@ -151,6 +160,11 @@ export const createPublishGithubPullRequestAction = ({ title: 'Repository Subdirectory', description: 'Subdirectory of repository to apply changes to', }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitHub', + }, }, }, output: { @@ -173,6 +187,7 @@ export const createPublishGithubPullRequestAction = ({ description, targetPath, sourcePath, + token: providedToken, } = ctx.input; const { owner, repo, host } = parseRepoUrl(repoUrl, integrations); @@ -189,7 +204,9 @@ export const createPublishGithubPullRequestAction = ({ host, owner, repo, + token: providedToken, }); + const fileRoot = sourcePath ? resolveSafeChildPath(ctx.workspacePath, sourcePath) : ctx.workspacePath; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts index 2cd27da359..a32fea8830 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.test.ts @@ -96,6 +96,28 @@ describe('publish:gitlab', () => { ).rejects.toThrow(/No token available for host/); }); + it('should work when there is a token provided through ctx.input', async () => { + mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); + mockGitlabClient.Projects.create.mockResolvedValue({ + http_url_to_repo: 'http://mockurl.git', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'hosted.gitlab.com?repo=bob&owner=owner', + token: 'token', + }, + }); + + expect(mockGitlabClient.Namespaces.show).toHaveBeenCalledWith('owner'); + expect(mockGitlabClient.Projects.create).toHaveBeenCalledWith({ + namespace_id: 1234, + name: 'bob', + visibility: 'private', + }); + }); + it('should call the correct Gitlab APIs when the owner is an organization', async () => { mockGitlabClient.Namespaces.show.mockResolvedValue({ id: 1234 }); mockGitlabClient.Projects.create.mockResolvedValue({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts index 5941f52746..48979306b1 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlab.ts @@ -33,6 +33,7 @@ export function createPublishGitlabAction(options: { defaultBranch?: string; repoVisibility: 'private' | 'internal' | 'public'; sourcePath?: string; + token?: string; }>({ id: 'publish:gitlab', description: @@ -57,10 +58,16 @@ export function createPublishGitlabAction(options: { description: `Sets the default branch on the repository. The default value is 'master'`, }, sourcePath: { - title: + title: 'Source Path', + description: 'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.', type: 'string', }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitLab', + }, }, }, output: { @@ -100,13 +107,16 @@ export function createPublishGitlabAction(options: { ); } - if (!integrationConfig.config.token) { + if (!integrationConfig.config.token && !ctx.input.token) { throw new InputError(`No token available for host ${host}`); } + const token = ctx.input.token || integrationConfig.config.token!; + const tokenType = ctx.input.token ? 'oauthToken' : 'token'; + const client = new Gitlab({ host: integrationConfig.config.baseUrl, - token: integrationConfig.config.token, + [tokenType]: token, }); let { id: targetNamespace } = (await client.Namespaces.show(owner)) as { @@ -140,7 +150,7 @@ export function createPublishGitlabAction(options: { defaultBranch, auth: { username: 'oauth2', - password: integrationConfig.config.token, + password: token, }, logger: ctx.logger, commitMessage: config.getOptionalString( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts index 2ac369249b..bf687268c3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -31,6 +31,7 @@ export type GitlabMergeRequestActionInput = { description: string; branchName: string; targetPath: string; + token?: string; }; export const createPublishGitlabMergeRequestAction = (options: { @@ -75,6 +76,11 @@ export const createPublishGitlabMergeRequestAction = (options: { title: 'Repository Subdirectory', description: 'Subdirectory of repository to apply changes to', }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitLab', + }, }, }, output: { @@ -107,13 +113,16 @@ export const createPublishGitlabMergeRequestAction = (options: { ); } - if (!integrationConfig.config.token) { + if (!integrationConfig.config.token && !ctx.input.token) { throw new InputError(`No token available for host ${host}`); } + const token = ctx.input.token ?? integrationConfig.config.token!; + const tokenType = ctx.input.token ? 'oauthToken' : 'token'; + const api = new Gitlab({ host: integrationConfig.config.baseUrl, - token: integrationConfig.config.token, + [tokenType]: token, }); const fileRoot = ctx.workspacePath; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 161d5b865b..1597c575ac 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -26,7 +26,6 @@ import { TaskContext, TaskSpec, TaskSecrets } from './types'; const realFiles = Object.fromEntries( [ - require.resolve('vm2/lib/fixasync'), resolvePackagePath( '@backstage/plugin-scaffolder-backend', 'assets', @@ -497,6 +496,60 @@ describe('DefaultWorkflowRunner', () => { expect.objectContaining({ secrets: { foo: 'bar' } }), ); }); + + it('should be able to template secrets into the input of an action', async () => { + const task = createMockTaskWithSpec( + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-mock-action', + input: { + b: '${{ secrets.foo }}', + }, + }, + ], + output: {}, + parameters: {}, + }, + { foo: 'bar' }, + ); + + await runner.execute(task); + + expect(fakeActionHandler).toHaveBeenCalledWith( + expect.objectContaining({ input: { b: 'bar' } }), + ); + }); + + it('does not allow templating of secrets as an output', async () => { + const task = createMockTaskWithSpec( + { + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-mock-action', + input: { + b: '${{ secrets.foo }}', + }, + }, + ], + output: { + b: '${{ secrets.foo }}', + }, + parameters: {}, + }, + { foo: 'bar' }, + ); + + const executedTask = await runner.execute(task); + + expect(executedTask.output.b).toBeUndefined(); + }); }); describe('filters', () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index 5089998834..889e94d000 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -53,6 +53,7 @@ type TemplateContext = { steps: { [stepName: string]: { output: { [outputName: string]: JsonValue } }; }; + secrets?: Record; }; const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta3 => { @@ -231,8 +232,14 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { const action = this.options.actionRegistry.get(step.action); const { taskLogger, streamLogger } = createStepLogger({ task, step }); + // Secrets are only passed when templating the input to actions for security reasons const input = - (step.input && this.render(step.input, context, renderTemplate)) ?? + (step.input && + this.render( + step.input, + { ...context, secrets: task.secrets ?? {} }, + renderTemplate, + )) ?? {}; if (action.schema?.input) { diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts index 6741ab3379..f86b1663a7 100644 --- a/plugins/scaffolder-backend/src/service/helpers.ts +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -16,9 +16,9 @@ import { Entity, - LOCATION_ANNOTATION, - parseLocationReference, - SOURCE_LOCATION_ANNOTATION, + ANNOTATION_LOCATION, + parseLocationRef, + ANNOTATION_SOURCE_LOCATION, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { assertError } from '@backstage/errors'; @@ -59,15 +59,15 @@ export async function getWorkingDirectory( * For file locations this will return a `file://` URL. */ export function getEntityBaseUrl(entity: Entity): string | undefined { - let location = entity.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION]; + let location = entity.metadata.annotations?.[ANNOTATION_SOURCE_LOCATION]; if (!location) { - location = entity.metadata.annotations?.[LOCATION_ANNOTATION]; + location = entity.metadata.annotations?.[ANNOTATION_LOCATION]; } if (!location) { return undefined; } - const { type, target } = parseLocationReference(location); + const { type, target } = parseLocationRef(location); if (type === 'url') { return target; } else if (type === 'file') { diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index d37219fe97..6b03a7c884 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -40,6 +40,6 @@ "@backstage/types": "^0.1.1" }, "devDependencies": { - "@backstage/cli": "^0.13.0" + "@backstage/cli": "^0.13.2-next.0" } } diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 81fd54b5aa..dd7674a21b 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-scaffolder +## 0.12.2 + +### Patch Changes + +- 33e139e652: Adds a loading bar to the scaffolder task page if the task is still loading. This can happen if it takes a while for a task worker to pick up a task. +- 6458be3307: Encode the `formData` in the `queryString` using `JSON.stringify` to keep the types in the decoded value +- 319f4b79a2: The ScaffolderPage can be passed an optional `TaskPageComponent` with a `loadingText` string. It will replace the Loading text in the scaffolder task page. +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + - @backstage/integration-react@0.1.21 + +## 0.12.2-next.0 + +### Patch Changes + +- 33e139e652: Adds a loading bar to the scaffolder task page if the task is still loading. This can happen if it takes a while for a task worker to pick up a task. +- 6458be3307: Encode the `formData` in the `queryString` using `JSON.stringify` to keep the types in the decoded value +- 319f4b79a2: The ScaffolderPage can be passed an optional `TaskPageComponent` with a `loadingText` string. It will replace the Loading text in the scaffolder task page. +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + - @backstage/integration-react@0.1.21-next.0 + +## 0.12.1 + +### Patch Changes + +- ba59832aed: Permission the Register Existing Component button +- cee44ad289: Added the ability to collect users `oauth` token from the `RepoUrlPicker` for use in the template manifest +- a681cb9c2f: Make linkTarget configurable for MarkdownContent component +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + - @backstage/plugin-catalog-common@0.1.2 + - @backstage/integration-react@0.1.20 + +## 0.12.1-next.1 + +### Patch Changes + +- ba59832aed: Permission the Register Existing Component button +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + - @backstage/plugin-catalog-react@0.6.13-next.1 + - @backstage/plugin-catalog-common@0.1.2-next.0 + +## 0.12.1-next.0 + +### Patch Changes + +- a681cb9c2f: Make linkTarget configurable for MarkdownContent component +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/integration-react@0.1.20-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.12.0 ### Minor Changes diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index b77fb5f939..f8ae64cd82 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -164,6 +164,16 @@ export interface RepoUrlPickerUiOptions { allowedHosts?: string[]; // (undocumented) allowedOwners?: string[]; + // (undocumented) + requestUserCredentials?: { + secretsKey: string; + additionalScopes?: { + github?: string[]; + gitlab?: string[]; + bitbucket?: string[]; + azure?: string[]; + }; + }; } // @public @@ -245,6 +255,7 @@ export const ScaffolderFieldExtensions: React_2.ComponentType; // @public (undocumented) export const ScaffolderPage: ({ TemplateCardComponent, + TaskPageComponent, groups, }: { TemplateCardComponent?: @@ -252,6 +263,7 @@ export const ScaffolderPage: ({ template: TemplateEntityV1beta2; }> | undefined; + TaskPageComponent?: ComponentType<{}> | undefined; groups?: | { title?: string | undefined; @@ -275,6 +287,14 @@ const scaffolderPlugin: BackstagePlugin< export { scaffolderPlugin as plugin }; export { scaffolderPlugin }; +// @public +export const TaskPage: ({ loadingText }: TaskPageProps) => JSX.Element; + +// @public +export type TaskPageProps = { + loadingText?: string; +}; + // Warning: (ae-missing-release-tag) "TemplateList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -317,4 +337,9 @@ export const TextValuePicker: ({ idSchema, placeholder, }: FieldProps) => JSX.Element; + +// @public +export const useTemplateSecrets: () => { + setSecret: (input: Record) => void; +}; ``` diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index a5c6e78f13..0e31d6c4a5 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "0.12.0", + "version": "0.12.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,15 +31,17 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.5.5", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", "@backstage/integration": "^0.7.2", - "@backstage/integration-react": "^0.1.19", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/integration-react": "^0.1.21", + "@backstage/plugin-catalog-common": "^0.1.2", + "@backstage/plugin-catalog-react": "^0.6.14", + "@backstage/plugin-permission-react": "^0.3.0", "@backstage/plugin-scaffolder-common": "^0.1.3", "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", @@ -67,11 +69,11 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/plugin-catalog": "^0.7.10", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/plugin-catalog": "^0.7.12", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -79,7 +81,7 @@ "@types/humanize-duration": "^3.18.1", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "event-source-polyfill": "^1.0.25", "msw": "^0.35.0" }, diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index e667c676f2..601844780d 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -21,6 +21,7 @@ import { ScaffolderPage } from './ScaffolderPage'; import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; import { ActionsPage } from './ActionsPage'; +import { SecretsContextProvider } from './secrets/SecretsContext'; import { FieldExtensionOptions, @@ -34,6 +35,7 @@ type RouterProps = { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2 }> | undefined; + TaskPageComponent?: ComponentType<{}>; groups?: Array<{ title?: string; titleComponent?: React.ReactNode; @@ -41,8 +43,13 @@ type RouterProps = { }>; }; -export const Router = ({ TemplateCardComponent, groups }: RouterProps) => { +export const Router = ({ + TemplateCardComponent, + TaskPageComponent, + groups, +}: RouterProps) => { const outlet = useOutlet(); + const TaskPageElement = TaskPageComponent || TaskPage; const customFieldExtensions = useElementFilter(outlet, elements => elements @@ -77,9 +84,13 @@ export const Router = ({ TemplateCardComponent, groups }: RouterProps) => { /> } + element={ + + + + } /> - } /> + } /> } /> ); diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index c147c8e118..116b7ed82a 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -37,6 +37,8 @@ import React, { ComponentType } from 'react'; import { registerComponentRouteRef } from '../../routes'; import { TemplateList } from '../TemplateList'; import { TemplateTypePicker } from '../TemplateTypePicker'; +import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common'; +import { usePermission } from '@backstage/plugin-permission-react'; const useStyles = makeStyles(theme => ({ contentWrapper: { @@ -72,6 +74,8 @@ export const ScaffolderPageContents = ({ }, }; + const { allowed } = usePermission(catalogEntityCreatePermission); + return (
- + {allowed && ( + + )} Create new software components using standard templates. Different templates create different kinds of components (services, websites, diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index 9f5dad2adc..2f49f40643 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -21,6 +21,7 @@ import { Lifecycle, Page, LogViewer, + Progress, } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; @@ -218,7 +219,23 @@ export const TaskStatusStepper = memo( const hasLinks = ({ entityRef, remoteUrl, links = [] }: TaskOutput): boolean => !!(entityRef || remoteUrl || links.length > 0); -export const TaskPage = () => { +/** + * TaskPageProps for constructing a TaskPage + * @param loadingText - Optional loading text shown before a task begins executing. + * + * @public + */ +export type TaskPageProps = { + loadingText?: string; +}; + +/** + * TaskPage for showing the status of the taskId provided as a param + * @param loadingText - Optional loading text shown before a task begins executing. + * + * @public + */ +export const TaskPage = ({ loadingText }: TaskPageProps) => { const classes = useStyles(); const navigate = useNavigate(); const rootLink = useRouteRef(rootRouteRef); @@ -256,7 +273,7 @@ export const TaskPage = () => { const logAsString = useMemo(() => { if (!currentStepId) { - return 'Loading...'; + return loadingText ? loadingText : 'Loading...'; } const log = taskStream.stepLogs[currentStepId]; @@ -264,7 +281,7 @@ export const TaskPage = () => { return 'Waiting for logs...'; } return log.join('\n'); - }, [taskStream.stepLogs, currentStepId]); + }, [taskStream.stepLogs, currentStepId, loadingText]); const taskNotFound = taskStream.completed === true && @@ -285,7 +302,9 @@ export const TaskPage = () => { navigate( generatePath( - `${rootLink()}/templates/:templateName?${qs.stringify({ formData })}`, + `${rootLink()}/templates/:templateName?${qs.stringify({ + formData: JSON.stringify(formData), + })}`, { templateName: taskStream.task!.spec.metadata!.name, }, @@ -336,6 +355,8 @@ export const TaskPage = () => { + {!currentStepId && } +
diff --git a/plugins/scaffolder/src/components/TaskPage/index.ts b/plugins/scaffolder/src/components/TaskPage/index.ts index 809b45101b..f7876888f9 100644 --- a/plugins/scaffolder/src/components/TaskPage/index.ts +++ b/plugins/scaffolder/src/components/TaskPage/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { TaskPage } from './TaskPage'; +export type { TaskPageProps } from './TaskPage'; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 8ce7ecf6b3..d30c292053 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -17,12 +17,13 @@ import { JsonObject, JsonValue } from '@backstage/types'; import { LinearProgress } from '@material-ui/core'; import { FormValidation, IChangeEvent } from '@rjsf/core'; import qs from 'qs'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useContext, useState } from 'react'; import { generatePath, Navigate, useNavigate } from 'react-router'; import { useParams } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; import { scaffolderApiRef } from '../../api'; import { CustomFieldValidator, FieldExtensionOptions } from '../../extensions'; +import { SecretsContext } from '../secrets/SecretsContext'; import { rootRouteRef } from '../../routes'; import { MultistepJsonForm } from '../MultistepJsonForm'; @@ -115,6 +116,7 @@ export const TemplatePage = ({ customFieldExtensions?: FieldExtensionOptions[]; }) => { const apiHolder = useApiHolder(); + const secretsContext = useContext(SecretsContext); const errorApi = useApi(errorApiRef); const scaffolderApi = useApi(scaffolderApiRef); const { templateName } = useParams(); @@ -126,7 +128,11 @@ export const TemplatePage = ({ ignoreQueryPrefix: true, }); - return query.formData ?? {}; + try { + return JSON.parse(query.formData as string); + } catch (e) { + return query.formData ?? {}; + } }); const handleFormReset = () => setFormState({}); const handleChange = useCallback( @@ -135,7 +141,11 @@ export const TemplatePage = ({ ); const handleCreate = async () => { - const id = await scaffolderApi.scaffold(templateName, formState); + const id = await scaffolderApi.scaffold( + templateName, + formState, + secretsContext?.secrets, + ); const formParams = qs.stringify( { formData: formState }, diff --git a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx index b0577acfeb..bf566bd616 100644 --- a/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityTagsPicker/EntityTagsPicker.tsx @@ -16,7 +16,7 @@ import React, { useState } from 'react'; import useAsync from 'react-use/lib/useAsync'; import useEffectOnce from 'react-use/lib/useEffectOnce'; -import { CatalogEntitiesRequest } from '@backstage/catalog-client'; +import { GetEntitiesRequest } from '@backstage/catalog-client'; import { Entity, makeValidator } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; @@ -40,7 +40,7 @@ export const EntityTagsPicker = ({ const kinds = uiSchema['ui:options']?.kinds as string[]; const { loading, value: existingTags } = useAsync(async () => { - const tagsRequest: CatalogEntitiesRequest = { fields: ['metadata.tags'] }; + const tagsRequest: GetEntitiesRequest = { fields: ['metadata.tags'] }; if (kinds) { tagsRequest.filter = { kind: kinds }; } diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx new file mode 100644 index 0000000000..829495cdda --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.test.tsx @@ -0,0 +1,177 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useContext } from 'react'; +import { RepoUrlPicker } from './RepoUrlPicker'; +import Form from '@rjsf/core'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + scmIntegrationsApiRef, + ScmIntegrationsApi, + scmAuthApiRef, + ScmAuthApi, +} from '@backstage/integration-react'; +import { scaffolderApiRef, ScaffolderApi } from '../../../api'; +import { + SecretsContextProvider, + SecretsContext, +} from '../../secrets/SecretsContext'; +import { act, fireEvent } from '@testing-library/react'; + +describe('RepoUrlPicker', () => { + const mockScaffolderApi: Partial = { + getIntegrationsList: async () => [ + { host: 'github.com', type: 'github', title: 'github.com' }, + { host: 'dev.azure.com', type: 'azure', title: 'dev.azure.com' }, + ], + }; + + const mockIntegrationsApi: Partial = { + byHost: () => ({ type: 'github' }), + }; + + const mockScmAuthApi: Partial = { + getCredentials: jest.fn().mockResolvedValue({ token: 'abc123' }), + }; + + describe('happy path rendering', () => { + it('should render the repo url picker with minimal props', async () => { + const onSubmit = jest.fn(); + const { getAllByRole, getByRole } = await renderInTestApp( + + +
+ + , + ); + + const [ownerInput, repoInput] = getAllByRole('textbox'); + const submitButton = getByRole('button'); + + fireEvent.change(ownerInput, { target: { value: 'backstage' } }); + fireEvent.change(repoInput, { target: { value: 'repo123' } }); + + fireEvent.click(submitButton); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + formData: 'github.com?owner=backstage&repo=repo123', + }), + expect.anything(), + ); + }); + + it('should render properly with allowedHosts', async () => { + const { getByRole } = await renderInTestApp( + + + + + , + ); + + expect( + getByRole('option', { name: 'dev.azure.com' }), + ).toBeInTheDocument(); + }); + }); + + describe('requestUserCredentials', () => { + it('should call the scmAuthApi with the correct params', async () => { + const SecretsComponent = () => { + const value = useContext(SecretsContext); + return
{JSON.stringify(value)}
; + }; + const { getAllByRole, getByTestId } = await renderInTestApp( + + + + + + , + ); + + const [ownerInput, repoInput] = getAllByRole('textbox'); + + await act(async () => { + fireEvent.change(ownerInput, { target: { value: 'backstage' } }); + fireEvent.change(repoInput, { target: { value: 'repo123' } }); + + // need to wait for the debounce to finish + await new Promise(resolve => setTimeout(resolve, 600)); + }); + + expect(mockScmAuthApi.getCredentials).toHaveBeenCalledWith({ + url: 'https://github.com/backstage/repo123', + additionalScope: { + repoWrite: true, + customScopes: { + github: ['workflow:write'], + }, + }, + }); + + const currentSecrets = JSON.parse( + getByTestId('current-secrets').textContent!, + ); + + expect(currentSecrets).toEqual({ + secrets: { testKey: 'abc123' }, + }); + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index 998fa41588..1ccaed4e86 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -14,7 +14,10 @@ * limitations under the License. */ import { useApi } from '@backstage/core-plugin-api'; -import { scmIntegrationsApiRef } from '@backstage/integration-react'; +import { + scmIntegrationsApiRef, + scmAuthApiRef, +} from '@backstage/integration-react'; import React, { useEffect, useState, useMemo, useCallback } from 'react'; import { GithubRepoPicker } from './GithubRepoPicker'; import { GitlabRepoPicker } from './GitlabRepoPicker'; @@ -24,10 +27,21 @@ import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; import { RepoUrlPickerState } from './types'; +import useDebounce from 'react-use/lib/useDebounce'; +import { useTemplateSecrets } from '../../secrets'; export interface RepoUrlPickerUiOptions { allowedHosts?: string[]; allowedOwners?: string[]; + requestUserCredentials?: { + secretsKey: string; + additionalScopes?: { + github?: string[]; + gitlab?: string[]; + bitbucket?: string[]; + azure?: string[]; + }; + }; } export const RepoUrlPicker = ( @@ -38,7 +52,8 @@ export const RepoUrlPicker = ( parseRepoPickerUrl(formData), ); const integrationApi = useApi(scmIntegrationsApiRef); - + const scmAuthApi = useApi(scmAuthApiRef); + const { setSecret } = useTemplateSecrets(); const allowedHosts = useMemo( () => uiSchema?.['ui:options']?.allowedHosts ?? [], [uiSchema], @@ -66,6 +81,42 @@ export const RepoUrlPicker = ( [setState], ); + useDebounce( + async () => { + const { requestUserCredentials } = uiSchema?.['ui:options'] ?? {}; + + if ( + !requestUserCredentials || + !(state.host && state.owner && state.repoName) + ) { + return; + } + + const [host, owner, repoName] = [ + state.host, + state.owner, + state.repoName, + ].map(encodeURIComponent); + + // user has requested that we use the users credentials + // so lets grab them using the scmAuthApi and pass through + // any additional scopes from the ui:options + const { token } = await scmAuthApi.getCredentials({ + url: `https://${host}/${owner}/${repoName}`, + additionalScope: { + repoWrite: true, + customScopes: requestUserCredentials.additionalScopes, + }, + }); + + // set the secret using the key provided in the the ui:options for use + // in the templating the manifest with ${{ secrets[secretsKey] }} + setSecret({ [requestUserCredentials.secretsKey]: token }); + }, + 500, + [state, uiSchema], + ); + const hostType = (state.host && integrationApi.byHost(state.host)?.type) ?? null; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx index 261429c757..ec09494a1b 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPickerHost.tsx @@ -37,16 +37,23 @@ export const RepoUrlPickerHost = (props: { }); useEffect(() => { - if (hosts && !host) { - // This is only hear to set the default as the first one in the hosts array - // if the host is not set yet and there is a list of hosts. - onChange(hosts[0]); + // If there is no host chosen currently + if (!host) { + // Set the first of the allowedHosts option if that available + if (hosts?.length) { + onChange(hosts[0]); + // if there's no hosts provided, fallback to using the first integration + } else if (integrations?.length) { + onChange(integrations[0].host); + } } - }, [hosts, host, onChange]); + }, [hosts, host, onChange, integrations]); + // If there are no allowedHosts provided, then show all integrations. Otherwise, only show integrations + // that are provided in the dropdown for the user to choose from. const hostsOptions: SelectItem[] = integrations ? integrations - .filter(i => hosts?.includes(i.host)) + .filter(i => (hosts?.length ? hosts?.includes(i.host) : true)) .map(i => ({ label: i.title, value: i.host })) : [{ label: 'Loading...', value: 'loading' }]; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts index d1f2302d3e..2f17f0aef7 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/utils.ts @@ -44,22 +44,22 @@ export function serializeRepoPickerUrl(data: RepoUrlPickerState) { export function parseRepoPickerUrl( url: string | undefined, ): RepoUrlPickerState { - let host = undefined; - let owner = undefined; - let repoName = undefined; - let organization = undefined; - let workspace = undefined; - let project = undefined; + let host = ''; + let owner = ''; + let repoName = ''; + let organization = ''; + let workspace = ''; + let project = ''; try { if (url) { const parsed = new URL(`https://${url}`); host = parsed.host; - owner = parsed.searchParams.get('owner') || undefined; - repoName = parsed.searchParams.get('repo') || undefined; - organization = parsed.searchParams.get('organization') || undefined; - workspace = parsed.searchParams.get('workspace') || undefined; - project = parsed.searchParams.get('project') || undefined; + owner = parsed.searchParams.get('owner') || ''; + repoName = parsed.searchParams.get('repo') || ''; + organization = parsed.searchParams.get('organization') || ''; + workspace = parsed.searchParams.get('workspace') || ''; + project = parsed.searchParams.get('project') || ''; } } catch { /* ok */ diff --git a/plugins/scaffolder/src/components/secrets/SecretsContext.test.tsx b/plugins/scaffolder/src/components/secrets/SecretsContext.test.tsx new file mode 100644 index 0000000000..37d35c9015 --- /dev/null +++ b/plugins/scaffolder/src/components/secrets/SecretsContext.test.tsx @@ -0,0 +1,43 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useContext } from 'react'; +import { + useTemplateSecrets, + SecretsContextProvider, + SecretsContext, +} from './SecretsContext'; +import { renderHook, act } from '@testing-library/react-hooks'; + +describe('SecretsContext', () => { + it('should allow the setting of secrets in the context', async () => { + const { result } = renderHook( + () => ({ + hook: useTemplateSecrets(), + context: useContext(SecretsContext), + }), + { + wrapper: ({ children }) => ( + {children} + ), + }, + ); + expect(result.current.context?.secrets.foo).toEqual(undefined); + + act(() => result.current.hook.setSecret({ foo: 'bar' })); + + expect(result.current.context?.secrets.foo).toEqual('bar'); + }); +}); diff --git a/plugins/scaffolder/src/components/secrets/SecretsContext.tsx b/plugins/scaffolder/src/components/secrets/SecretsContext.tsx new file mode 100644 index 0000000000..6570f9d901 --- /dev/null +++ b/plugins/scaffolder/src/components/secrets/SecretsContext.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { + useState, + useCallback, + useContext, + createContext, + PropsWithChildren, +} from 'react'; + +type SecretsContextContents = { + secrets: Record; + setSecrets: React.Dispatch>>; +}; + +/** + * The actual context object. + */ +export const SecretsContext = createContext( + undefined, +); + +/** + * The Context Provider that holds the state for the secrets. + * + * @public + */ +export const SecretsContextProvider = ({ children }: PropsWithChildren<{}>) => { + const [secrets, setSecrets] = useState>({}); + + return ( + + {children} + + ); +}; + +/** + * Hook to access the secrets context. + * @public + */ +export const useTemplateSecrets = () => { + const value = useContext(SecretsContext); + if (!value) { + throw new Error( + 'useTemplateSecrets must be used within a SecretsContextProvider', + ); + } + + const { setSecrets } = value; + + const setSecret = useCallback( + (input: Record) => { + setSecrets(currentSecrets => ({ ...currentSecrets, ...input })); + }, + [setSecrets], + ); + + return { setSecret }; +}; diff --git a/plugins/scaffolder/src/components/secrets/index.ts b/plugins/scaffolder/src/components/secrets/index.ts new file mode 100644 index 0000000000..44adb2a00a --- /dev/null +++ b/plugins/scaffolder/src/components/secrets/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { useTemplateSecrets } from './SecretsContext'; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 3cf65d7a07..adb2aab035 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -56,3 +56,6 @@ export { FavouriteTemplate } from './components/FavouriteTemplate'; export { TemplateList } from './components/TemplateList'; export type { TemplateListProps } from './components/TemplateList'; export { TemplateTypePicker } from './components/TemplateTypePicker'; +export * from './components/secrets'; +export { TaskPage } from './components/TaskPage'; +export type { TaskPageProps } from './components/TaskPage'; diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 42944fba0d..8a0e0d9d2c 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -30,9 +30,9 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/cli": "^0.13.0", - "@elastic/elasticsearch-mock": "^0.3.0" + "@backstage/backend-common": "^0.10.7-next.0", + "@backstage/cli": "^0.13.2-next.0", + "@elastic/elasticsearch-mock": "^1.0.0" }, "files": [ "dist", diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 8abeaa10e8..c0b1f6778d 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-search-backend-module-pg +## 0.2.6 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7 + +## 0.2.6-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.2.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + ## 0.2.4 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 2b7b9bbc77..392b9af73a 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-pg", "description": "A module for the search backend that implements search using PostgreSQL", - "version": "0.2.4", + "version": "0.2.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,15 +20,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/search-common": "^0.2.2", "@backstage/plugin-search-backend-node": "^0.4.5", "lodash": "^4.17.21", - "knex": "^0.95.1" + "knex": "^1.0.2" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.15", - "@backstage/cli": "^0.13.0" + "@backstage/backend-test-utils": "^0.1.17", + "@backstage/cli": "^0.13.2" }, "files": [ "dist", diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 7610c054c5..e2d83d0f4f 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -26,8 +26,8 @@ "@types/lunr": "^2.3.3" }, "devDependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/cli": "^0.13.0" + "@backstage/backend-common": "^0.10.7-next.0", + "@backstage/cli": "^0.13.2-next.0" }, "files": [ "dist" diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index ed7b0811ad..218615c66b 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,50 @@ # @backstage/plugin-search-backend +## 0.4.2 + +### Patch Changes + +- b3f3e42036: Use `getBearerTokenFromAuthorizationHeader` from `@backstage/plugin-auth-node` instead of the deprecated `IdentityClient` method. +- Updated dependencies + - @backstage/backend-common@0.10.7 + - @backstage/plugin-auth-node@0.1.0 + - @backstage/plugin-permission-node@0.4.3 + +## 0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.10.0-next.0 + - @backstage/backend-common@0.10.7-next.0 + - @backstage/plugin-permission-node@0.4.3-next.0 + +## 0.4.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0 + - @backstage/backend-common@0.10.6 + - @backstage/plugin-permission-node@0.4.2 + +## 0.4.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0-next.1 + - @backstage/backend-common@0.10.6-next.0 + - @backstage/plugin-permission-node@0.4.2-next.1 + +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend@0.9.0-next.0 + - @backstage/plugin-permission-node@0.4.2-next.0 + ## 0.4.0 ### Minor Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index d3292e88bd..117e484c62 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "0.4.0", + "version": "0.4.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,13 +20,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", "@backstage/search-common": "^0.2.2", - "@backstage/plugin-auth-backend": "^0.8.0", + "@backstage/plugin-auth-node": "^0.1.0", "@backstage/plugin-permission-common": "^0.4.0-next.0", - "@backstage/plugin-permission-node": "^0.4.1", + "@backstage/plugin-permission-node": "^0.4.3", "@backstage/plugin-search-backend-node": "^0.4.5", "@backstage/types": "^0.1.1", "@types/express": "^4.17.6", @@ -40,7 +40,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 28a3247919..3f25c4f24a 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -22,7 +22,7 @@ import { errorHandler } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; import { Config } from '@backstage/config'; import { JsonObject, JsonValue } from '@backstage/types'; -import { IdentityClient } from '@backstage/plugin-auth-backend'; +import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; import { PermissionAuthorizer } from '@backstage/plugin-permission-common'; import { DocumentTypeInfo, SearchResultSet } from '@backstage/search-common'; import { SearchEngine } from '@backstage/plugin-search-backend-node'; @@ -106,7 +106,9 @@ export async function createRouter( }, pageCursor=${query.pageCursor ?? ''}`, ); - const token = IdentityClient.getBearerToken(req.header('authorization')); + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); try { const resultSet = await engine?.query(query, { token }); diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 412edde26c..bb4877b6da 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-search +## 0.6.2 + +### Patch Changes + +- faf49ba82f: Modify modal search to clamp result length to 5 rows. +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.6.2-next.0 + +### Patch Changes + +- faf49ba82f: Modify modal search to clamp result length to 5 rows. +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.6.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.6.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.6.0 ### Minor Changes diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index fc82e2be47..10ce787a6c 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -27,10 +27,12 @@ export const DefaultResultListItem: ({ result, icon, secondaryAction, + lineClamp, }: { icon?: ReactNode; secondaryAction?: ReactNode; result: IndexableDocument; + lineClamp?: number | undefined; }) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "FiltersProps" needs to be exported by the entry point index.d.ts @@ -143,9 +145,17 @@ export const SearchContextProvider: ({ initialState, children, }: React_2.PropsWithChildren<{ - initialState?: SettableSearchContext | undefined; + initialState?: SearchContextState | undefined; }>) => JSX.Element; +// @public +export type SearchContextState = { + term: string; + types: string[]; + filters: JsonObject; + pageCursor?: string; +}; + // Warning: (ae-missing-release-tag) "SearchFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -199,6 +209,7 @@ export type SearchFilterWrapperProps = SearchFilterComponentProps & { // @public (undocumented) export const SearchModal: ({ open, + hidden, toggleModal, }: SearchModalProps) => JSX.Element; @@ -206,9 +217,8 @@ export const SearchModal: ({ // // @public (undocumented) export interface SearchModalProps { - // (undocumented) + hidden?: boolean; open?: boolean; - // (undocumented) toggleModal: () => void; } @@ -318,7 +328,13 @@ export type SidebarSearchProps = { // @public (undocumented) export const useSearch: () => SearchContextValue; -// Warnings were encountered during analysis: -// -// src/components/SearchContext/SearchContext.d.ts:23:5 - (ae-forgotten-export) The symbol "SettableSearchContext" needs to be exported by the entry point index.d.ts +// @public +export function useSearchModal(initialState?: boolean): { + state: { + hidden: boolean; + open: boolean; + }; + toggleModal: () => void; + setOpen: (open: boolean) => void; +}; ``` diff --git a/plugins/search/package.json b/plugins/search/package.json index 478b49d072..34baf41306 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "0.6.0", + "version": "0.6.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "dependencies": { "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/search-common": "^0.2.2", "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", @@ -45,6 +45,7 @@ "qs": "^6.9.4", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", + "react-text-truncate": "^0.17.0", "react-use": "^17.2.4" }, "peerDependencies": { @@ -52,17 +53,17 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.jsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx similarity index 88% rename from plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.jsx rename to plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx index 0a8e413107..68f00f9301 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.jsx +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx @@ -20,6 +20,11 @@ import { renderInTestApp } from '@backstage/test-utils'; import FindInPageIcon from '@material-ui/icons/FindInPage'; import { DefaultResultListItem } from './DefaultResultListItem'; +// Using canvas to render text.. +jest.mock('react-text-truncate', () => { + return ({ text }: { text: string }) => {text}; +}); + describe('DefaultResultListItem', () => { const result = { title: 'title', @@ -44,10 +49,10 @@ describe('DefaultResultListItem', () => { await renderInTestApp( } + icon={} />, ); - expect(screen.getByTitle('icon')).toBeInTheDocument(); + expect(screen.getByLabelText('icon')).toBeInTheDocument(); }); it('should render secondary action if prop is specified', async () => { diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx index dd541b6e21..a6c9a36bb6 100644 --- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -24,17 +24,20 @@ import { Divider, } from '@material-ui/core'; import { Link } from '@backstage/core-components'; +import TextTruncate from 'react-text-truncate'; type Props = { icon?: ReactNode; secondaryAction?: ReactNode; result: IndexableDocument; + lineClamp?: number; }; export const DefaultResultListItem = ({ result, icon, secondaryAction, + lineClamp = 5, }: Props) => { return ( @@ -43,7 +46,14 @@ export const DefaultResultListItem = ({ + } /> {secondaryAction && {secondaryAction}} diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx index 2ddec22294..a203d27c93 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.tsx @@ -169,13 +169,16 @@ export type SearchBarProps = Partial; export const SearchBar = ({ onChange, ...props }: SearchBarProps) => { const { term, setTerm } = useSearch(); - const handleChange = (newValue: string) => { - if (onChange) { - onChange(newValue); - } else { - setTerm(newValue); - } - }; + const handleChange = useCallback( + (newValue: string) => { + if (onChange) { + onChange(newValue); + } else { + setTerm(newValue); + } + }, + [onChange, setTerm], + ); return ; }; diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx index 336041bf1f..a9f42fe5ca 100644 --- a/plugins/search/src/components/SearchContext/SearchContext.tsx +++ b/plugins/search/src/components/SearchContext/SearchContext.tsx @@ -31,45 +31,41 @@ import { searchApiRef } from '../../apis'; type SearchContextValue = { result: AsyncState; - term: string; setTerm: React.Dispatch>; - types: string[]; setTypes: React.Dispatch>; - filters: JsonObject; setFilters: React.Dispatch>; - open?: boolean; - toggleModal: () => void; - pageCursor?: string; setPageCursor: React.Dispatch>; fetchNextPage?: React.DispatchWithoutAction; fetchPreviousPage?: React.DispatchWithoutAction; -}; +} & SearchContextState; -type SettableSearchContext = Omit< - SearchContextValue, - | 'result' - | 'setTerm' - | 'setTypes' - | 'setFilters' - | 'toggleModal' - | 'setPageCursor' - | 'fetchNextPage' - | 'fetchPreviousPage' ->; +/** + * The initial state of `SearchContextProvider`. + * + * @public + */ +export type SearchContextState = { + term: string; + types: string[]; + filters: JsonObject; + pageCursor?: string; +}; export const SearchContext = createContext( undefined, ); +const searchInitialState: SearchContextState = { + term: '', + pageCursor: undefined, + filters: {}, + types: [], +}; + export const SearchContextProvider = ({ - initialState = { - term: '', - pageCursor: undefined, - filters: {}, - types: [], - }, + initialState = searchInitialState, children, -}: PropsWithChildren<{ initialState?: SettableSearchContext }>) => { +}: PropsWithChildren<{ initialState?: SearchContextState }>) => { const searchApi = useApi(searchApiRef); const [pageCursor, setPageCursor] = useState( initialState.pageCursor, @@ -77,11 +73,6 @@ export const SearchContextProvider = ({ const [filters, setFilters] = useState(initialState.filters); const [term, setTerm] = useState(initialState.term); const [types, setTypes] = useState(initialState.types); - const [open, setOpen] = useState(false); - const toggleModal = useCallback( - (): void => setOpen(prevState => !prevState), - [], - ); const prevTerm = usePrevious(term); @@ -90,7 +81,7 @@ export const SearchContextProvider = ({ searchApi.query({ term, filters, - pageCursor: pageCursor, + pageCursor, types, }), [term, filters, types, pageCursor], @@ -118,8 +109,6 @@ export const SearchContextProvider = ({ result, filters, setFilters, - open, - toggleModal, term, setTerm, types, diff --git a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx index e969e0d8cc..36d670a1ed 100644 --- a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx +++ b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx @@ -16,7 +16,7 @@ import { ApiProvider } from '@backstage/core-app-api'; import { SearchResultSet } from '@backstage/search-common'; import { TestApiRegistry } from '@backstage/test-utils'; -import React, { ComponentProps } from 'react'; +import React, { ComponentProps, PropsWithChildren } from 'react'; import { searchApiRef } from '../../apis'; import { SearchContextProvider as RealSearchContextProvider } from './SearchContext'; @@ -33,13 +33,16 @@ type QueryResultProps = { export const SearchContextProvider = ( props: ComponentProps & QueryResultProps, ) => { - const { mockedResults, ...contextProps } = props; - const query: any = () => Promise.resolve(mockedResults || {}); - const apiRegistry = TestApiRegistry.from([searchApiRef, { query }]); - return ( - - - + + + ); }; + +export function SearchApiProvider(props: PropsWithChildren) { + const { mockedResults, children } = props; + const query: any = () => Promise.resolve(mockedResults || {}); + const apiRegistry = TestApiRegistry.from([searchApiRef, { query }]); + return ; +} diff --git a/plugins/search/src/components/SearchContext/index.tsx b/plugins/search/src/components/SearchContext/index.tsx index 682ade916f..8651e661c1 100644 --- a/plugins/search/src/components/SearchContext/index.tsx +++ b/plugins/search/src/components/SearchContext/index.tsx @@ -19,3 +19,5 @@ export { SearchContext, useSearch, } from './SearchContext'; + +export type { SearchContextState } from './SearchContext'; diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx index e4b89000d5..aa0ad0479a 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx @@ -18,9 +18,9 @@ import { wrapInTestApp } from '@backstage/test-utils'; import { Button } from '@material-ui/core'; import React, { ComponentType } from 'react'; import { rootRouteRef } from '../../plugin'; -import { useSearch } from '../SearchContext'; -import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories'; +import { SearchApiProvider } from '../SearchContext/SearchContextForStorybook.stories'; import { SearchModal } from './SearchModal'; +import { useSearchModal } from './useSearchModal'; const mockResults = { results: [ @@ -57,23 +57,23 @@ export default { decorators: [ (Story: ComponentType<{}>) => wrapInTestApp( - + - , + , { mountedRoutes: { '/search': rootRouteRef } }, ), ], }; export const Default = () => { - const { open, toggleModal } = useSearch(); + const { state, toggleModal } = useSearchModal(); return ( <> - + ); }; diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index b59be710de..d3b2a825e2 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -24,7 +24,6 @@ import { rootRouteRef } from '../../plugin'; import { searchApiRef } from '../../apis'; import { SearchModal } from './SearchModal'; -import { SearchContextProvider } from '../SearchContext'; describe('SearchModal', () => { const query = jest.fn().mockResolvedValue({ results: [] }); @@ -34,14 +33,16 @@ describe('SearchModal', () => { [searchApiRef, { query }], ); + beforeEach(() => { + query.mockClear(); + }); + const toggleModal = jest.fn(); it('Should render the Modal correctly', async () => { await renderInTestApp( - - - + , { mountedRoutes: { @@ -51,14 +52,13 @@ describe('SearchModal', () => { ); expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(query).toHaveBeenCalledTimes(1); }); it('Calls toggleModal handler', async () => { await renderInTestApp( - - - + , { mountedRoutes: { @@ -66,7 +66,25 @@ describe('SearchModal', () => { }, }, ); + + expect(query).toHaveBeenCalledTimes(1); userEvent.keyboard('{esc}'); expect(toggleModal).toHaveBeenCalledTimes(1); }); + + it('should render SearchModal hiding its content', async () => { + const { getByTestId } = await renderInTestApp( + + , + { + mountedRoutes: { + '/search': rootRouteRef, + }, + }, + ); + + expect(getByTestId('search-bar-next')).toBeInTheDocument(); + expect(getByTestId('search-bar-next')).not.toBeVisible(); + }); }); diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index 1f0a879f96..2a71e53ea8 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -38,7 +38,21 @@ import { Link, useContent } from '@backstage/core-components'; import { rootRouteRef } from '../../plugin'; export interface SearchModalProps { + /** + * If true, it renders the modal. + */ open?: boolean; + /** + * This is supposed to be used together with the open prop. + * If `hidden` is true, it hides the modal. + * If `open` is false, the value of `hidden` has no effect on the modal. + * Use `open` for controlling whether the modal should be rendered or not. + */ + hidden?: boolean; + /** + * a function invoked when a search item is pressed or when the dialog + * should be closed. + */ toggleModal: () => void; } @@ -57,7 +71,7 @@ const useStyles = makeStyles(theme => ({ viewResultsLink: { verticalAlign: '0.5em' }, })); -export const Modal = ({ open = true, toggleModal }: SearchModalProps) => { +export const Modal = ({ toggleModal }: SearchModalProps) => { const getSearchLink = useRouteRef(rootRouteRef); const classes = useStyles(); @@ -75,16 +89,7 @@ export const Modal = ({ open = true, toggleModal }: SearchModalProps) => { }; return ( - + <> @@ -139,14 +144,34 @@ export const Modal = ({ open = true, toggleModal }: SearchModalProps) => { - + ); }; -export const SearchModal = ({ open = true, toggleModal }: SearchModalProps) => { +export const SearchModal = ({ + open = true, + hidden, + toggleModal, +}: SearchModalProps) => { + const classes = useStyles(); + return ( - - - + ); }; diff --git a/plugins/search/src/components/SearchModal/index.tsx b/plugins/search/src/components/SearchModal/index.tsx index ec0053951d..646dff0a62 100644 --- a/plugins/search/src/components/SearchModal/index.tsx +++ b/plugins/search/src/components/SearchModal/index.tsx @@ -15,3 +15,4 @@ */ export { SearchModal } from './SearchModal'; export type { SearchModalProps } from './SearchModal'; +export { useSearchModal } from './useSearchModal'; diff --git a/plugins/search/src/components/SearchModal/useSearchModal.test.tsx b/plugins/search/src/components/SearchModal/useSearchModal.test.tsx new file mode 100644 index 0000000000..424dffff76 --- /dev/null +++ b/plugins/search/src/components/SearchModal/useSearchModal.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { useSearchModal } from './useSearchModal'; + +describe('useSearchModal', () => { + it.each([ + [true, { open: true, hidden: false }], + [false, { open: false, hidden: true }], + ])( + 'should return the correct state when initial state is %s', + (initialState, result) => { + const rendered = renderHook(() => useSearchModal(initialState)); + + expect(rendered.result.current.state).toEqual(result); + }, + ); + + it('should keep open forever to true once modal is toggled', () => { + const rendered = renderHook(() => useSearchModal()); + act(() => rendered.result.current.toggleModal()); + + expect(rendered.result.current.state).toEqual({ + open: true, + hidden: false, + }); + + act(() => rendered.result.current.toggleModal()); + expect(rendered.result.current.state).toEqual({ + open: true, + hidden: true, + }); + }); + + it('should keep open to false if setOpen(false) is invoked on an initially closed modal', () => { + const rendered = renderHook(() => useSearchModal()); + act(() => rendered.result.current.setOpen(false)); + expect(rendered.result.current.state).toEqual({ + open: false, + hidden: true, + }); + }); + + it('should keep open forever to true even when the modal transition from opened to closed', () => { + const rendered = renderHook(() => useSearchModal()); + + act(() => rendered.result.current.setOpen(true)); + expect(rendered.result.current.state).toEqual({ + open: true, + hidden: false, + }); + + act(() => rendered.result.current.setOpen(false)); + expect(rendered.result.current.state).toEqual({ + open: true, + hidden: true, + }); + }); +}); diff --git a/plugins/search/src/components/SearchModal/useSearchModal.tsx b/plugins/search/src/components/SearchModal/useSearchModal.tsx new file mode 100644 index 0000000000..3871687d1a --- /dev/null +++ b/plugins/search/src/components/SearchModal/useSearchModal.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useCallback, useState } from 'react'; + +/** + * Use this hook to manage the state of {@link SearchModal} + * and change its visibility. + * + * @public + * + * @param initialState - pass `true` to make the modal initially visible + * @returns an object containing the state of the modal together with + * functions for changing the visibility of the modal. + */ +export function useSearchModal(initialState = false) { + const [state, setState] = useState({ + hidden: !initialState, + open: initialState, + }); + + const toggleModal = useCallback( + () => + setState(prevState => ({ + open: true, + hidden: !prevState.hidden, + })), + [], + ); + + const setOpen = useCallback( + (open: boolean) => + setState(prevState => ({ + open: prevState.open || open, + hidden: !open, + })), + [], + ); + + return { state, toggleModal, setOpen }; +} diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx index 6b4171a024..1aae64c463 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -33,7 +33,6 @@ describe('SearchType.Accordion', () => { term: '', types: [], filters: {}, - toggleModal: jest.fn(), setTerm: jest.fn(), setTypes: jest.fn(), setFilters: jest.fn(), diff --git a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx index 54f09023bc..741ee6e83f 100644 --- a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx @@ -33,7 +33,6 @@ describe('SearchType.Tabs', () => { term: '', types: [], filters: {}, - toggleModal: jest.fn(), setTerm: jest.fn(), setTypes: jest.fn(), setFilters: jest.fn(), diff --git a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx index b8f20de67d..bbf4eef4e4 100644 --- a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx +++ b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx @@ -17,15 +17,14 @@ import React from 'react'; import SearchIcon from '@material-ui/icons/Search'; import { SidebarItem } from '@backstage/core-components'; import { IconComponent } from '@backstage/core-plugin-api'; -import { SearchModal } from '../SearchModal'; -import { useSearch } from '../SearchContext'; +import { SearchModal, useSearchModal } from '../SearchModal'; export type SidebarSearchModalProps = { icon?: IconComponent; }; export const SidebarSearchModal = (props: SidebarSearchModalProps) => { - const { open, toggleModal } = useSearch(); + const { state, toggleModal } = useSearchModal(); const Icon = props.icon ? props.icon : SearchIcon; return ( @@ -36,7 +35,7 @@ export const SidebarSearchModal = (props: SidebarSearchModalProps) => { text="Search" onClick={toggleModal} /> - + ); }; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 99c3f5177b..cef0355fb4 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -32,13 +32,14 @@ export type { SearchBarProps, } from './components/SearchBar'; export { SearchContextProvider, useSearch } from './components/SearchContext'; +export type { SearchContextState } from './components/SearchContext'; export { SearchFilter, SearchFilterNext } from './components/SearchFilter'; export type { SearchAutocompleteFilterProps, SearchFilterComponentProps, SearchFilterWrapperProps, } from './components/SearchFilter'; -export { SearchModal } from './components/SearchModal'; +export { SearchModal, useSearchModal } from './components/SearchModal'; export type { SearchModalProps } from './components/SearchModal'; export { SearchPage as Router } from './components/SearchPage'; export { SearchResultPager } from './components/SearchResultPager'; diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 6e09dbf91a..5f87df9a3a 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-sentry +## 0.3.36 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.3.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.3.35 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.3.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.3.34 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index bcfaa33d41..48137dc5e1 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.3.34", + "version": "0.3.36", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,9 +33,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,10 +49,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -60,7 +60,7 @@ "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", "@types/react": "^16.13.1 || ^17.0.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index e9388c293e..11bc03104a 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-shortcuts +## 0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.1.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index a830943dbc..d3bcb14f15 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.1.20", + "version": "0.1.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,7 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", @@ -39,16 +39,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index d3b5ce4f2f..c0de393aab 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-sonarqube +## 0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.2.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.2.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.2.14 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 656430ce69..fb2de64ffe 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sonarqube", "description": "", - "version": "0.2.14", + "version": "0.2.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,15 +34,15 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@material-ui/styles": "^4.10.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "rc-progress": "3.2.4", "react-use": "^17.2.4" }, @@ -50,10 +50,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index b5e77459bc..badc0f06fc 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-splunk-on-call +## 0.3.22 + +### Patch Changes + +- c17be55ffb: Add Splunk On-Call plugin support for a `splunk.com/on-call-routing-key` annotation. If the `splunk.com/on-call-routing-key` is provided, the plugin displays a Splunk On-Call card for each of the teams associated with the routing key. +- 6c6d1c6439: Correct spelling of 'Acknowledge' in tooltip. +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.3.22-next.0 + +### Patch Changes + +- 6c6d1c6439: Correct spelling of 'Acknowledge' in tooltip. +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.3.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.3.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.3.20 ### Patch Changes diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md index 52803473b0..0200cacc50 100644 --- a/plugins/splunk-on-call/README.md +++ b/plugins/splunk-on-call/README.md @@ -2,16 +2,16 @@ ## Overview -This plugin displays Splunk On-Call, formerly VictorOps, information about an entity. +This plugin displays Splunk On-Call (formerly VictorOps) information associated with an entity. -There is a way to trigger an new incident directly to specific users or/and specific teams. +It also provides the ability to trigger new incidents to specific users and/or specific teams from within Backstage. -This plugin requires that entities are annotated with a team name. See more further down in this document. +This plugin requires that entities feature either a `splunk.com/on-call-team` or a `splunk.com/on-call-routing-key` annotation. See below for further details. This plugin provides: - A list of incidents -- A way to trigger a new incident to specific users or/and teams +- A way to trigger a new incident to specific users and/or teams - A way to acknowledge/resolve an incident - Information details about the persons on-call @@ -47,7 +47,7 @@ const overviewContent = ( ## Client configuration -In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide a REST Endpoint. +In order to be able to perform certain actions (create-acknowledge-resolve an action), you need to provide a REST Endpoint. To enable the REST Endpoint integration you can go on https://portal.victorops.com/ inside Integrations > 3rd Party Integrations > REST – Generic. You can now copy the URL to notify: `/$routing_key` @@ -76,12 +76,22 @@ In addition, to make certain API calls (trigger-resolve-acknowledge an incident) ### Adding your team name to the entity annotation -The information displayed for each entity is based on the team name. -If you want to use this plugin for an entity, you need to label it with the below annotation: +The information displayed for each entity is based on either an associated team name or an associated routing key. + +To use this plugin for an entity, the entity must be labeled with either a `splunk.com/on-call-team` or a `splunk.com/on-call-routing-key` annotation. + +For example, by specifying a `splunk.com/on-call-team`, the plugin displays Splunk On-Call data associated with the specified team: ```yaml annotations: - splunk.com/on-call-team': + splunk.com/on-call-team: +``` + +Alternatively, by specifying a `splunk.com/on-call-routing-key`, the plugin displays Splunk On-Call data associated with _each_ of the teams associated with the specified routing key: + +```yaml +annotations: + splunk.com/on-call-routing-key: ``` ### Create the Routing Key diff --git a/plugins/splunk-on-call/api-report.md b/plugins/splunk-on-call/api-report.md index 19f60ffd13..8e8076d9e8 100644 --- a/plugins/splunk-on-call/api-report.md +++ b/plugins/splunk-on-call/api-report.md @@ -51,6 +51,10 @@ export class SplunkOnCallClient implements SplunkOnCallApi { // // (undocumented) getOnCallUsers(): Promise; + // Warning: (ae-forgotten-export) The symbol "RoutingKey" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getRoutingKeys(): Promise; // Warning: (ae-forgotten-export) The symbol "Team" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index ada127bbfc..1bc8948d95 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-splunk-on-call", "description": "A Backstage plugin that integrates towards Splunk On-Call", - "version": "0.3.20", + "version": "0.3.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,17 +48,17 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "configSchema": "config.d.ts", diff --git a/plugins/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts index 437c6365ed..6c0ff9bdc4 100644 --- a/plugins/splunk-on-call/src/api/client.ts +++ b/plugins/splunk-on-call/src/api/client.ts @@ -19,6 +19,7 @@ import { OnCall, User, EscalationPolicyInfo, + RoutingKey, Team, } from '../components/types'; import { @@ -30,6 +31,7 @@ import { RequestOptions, ListUserResponse, EscalationPolicyResponse, + ListRoutingKeyResponse, } from './types'; import { createApiRef, @@ -82,6 +84,15 @@ export class SplunkOnCallClient implements SplunkOnCallApi { return teams; } + async getRoutingKeys(): Promise { + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/splunk-on-call/v1/org/routing-keys`; + const { routingKeys } = await this.getByUrl(url); + + return routingKeys; + } + async getUsers(): Promise { const url = `${await this.config.discoveryApi.getBaseUrl( 'proxy', diff --git a/plugins/splunk-on-call/src/api/mocks.ts b/plugins/splunk-on-call/src/api/mocks.ts index d5335bfef4..8db06fb9d3 100644 --- a/plugins/splunk-on-call/src/api/mocks.ts +++ b/plugins/splunk-on-call/src/api/mocks.ts @@ -17,6 +17,7 @@ import { EscalationPolicyInfo, Incident, + RoutingKey, Team, User, } from '../components/types'; @@ -88,6 +89,18 @@ export const MOCK_TEAM: Team = { isDefaultTeam: false, }; +export const MOCK_ROUTING_KEY: RoutingKey = { + routingKey: 'test-routing-key', + targets: [ + { + policyName: 'some policy', + policySlug: MOCK_TEAM.slug || '', + _teamUrl: `/api-public/v1/team/${MOCK_TEAM.slug}`, + }, + ], + isDefault: false, +}; + export const MOCK_TEAM_NO_INCIDENTS: Team = { ...MOCK_TEAM, name: 'test-noincidents', diff --git a/plugins/splunk-on-call/src/api/types.ts b/plugins/splunk-on-call/src/api/types.ts index 0bdd093ad1..6a414d18bd 100644 --- a/plugins/splunk-on-call/src/api/types.ts +++ b/plugins/splunk-on-call/src/api/types.ts @@ -18,6 +18,7 @@ import { EscalationPolicyInfo, Incident, OnCall, + RoutingKey, Team, User, } from '../components/types'; @@ -65,6 +66,11 @@ export interface SplunkOnCallApi { */ getTeams(): Promise; + /** + * Get a list of routing keys for your organization. + */ + getRoutingKeys(): Promise; + /** * Get a list of escalation policies for your organization. */ @@ -80,6 +86,11 @@ export type ListUserResponse = { _selfUrl?: string; }; +export type ListRoutingKeyResponse = { + routingKeys: RoutingKey[]; + _selfUrl?: string; +}; + export type IncidentsResponse = { incidents: Incident[]; }; diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index eb5c99d4e7..a83db136e4 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -28,6 +28,7 @@ import { MOCKED_ON_CALL, MOCKED_USER, MOCK_INCIDENT, + MOCK_ROUTING_KEY, MOCK_TEAM, MOCK_TEAM_NO_INCIDENTS, } from '../api/mocks'; @@ -45,6 +46,7 @@ const mockSplunkOnCallApi: Partial = { getIncidents: async () => [MOCK_INCIDENT], getOnCallUsers: async () => MOCKED_ON_CALL, getTeams: async () => [MOCK_TEAM], + getRoutingKeys: async () => [MOCK_ROUTING_KEY], getEscalationPolicies: async () => ESCALATION_POLICIES, }; @@ -71,6 +73,26 @@ const mockEntity = { }, } as Entity; +const mockEntityWithRoutingKeyAnnotation = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'splunkoncall-test', + annotations: { + 'splunk.com/on-call-routing-key': MOCK_ROUTING_KEY.routingKey, + }, + }, +} as Entity; + +const mockEntityNoAnnotation = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'splunkoncall-test', + annotations: {}, + }, +} as Entity; + const mockEntityNoIncidents = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -109,6 +131,34 @@ describe('SplunkOnCallCard', () => { expect(getByText('Empty escalation policy')).toBeInTheDocument(); }); + it('handles a "splunk.com/on-call-routing-key" annotation', async () => { + mockSplunkOnCallApi.getUsers = jest + .fn() + .mockImplementationOnce(async () => [MOCKED_USER]); + mockSplunkOnCallApi.getRoutingKeys = jest + .fn() + .mockImplementationOnce(async () => [MOCK_ROUTING_KEY]); + mockSplunkOnCallApi.getTeams = jest + .fn() + .mockImplementation(async () => [MOCK_TEAM]); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText(`Team: ${MOCK_TEAM.name}`)).toBeInTheDocument(); + await waitFor( + () => expect(getByText('test-incident')).toBeInTheDocument(), + { timeout: 2000 }, + ); + }); + it('Handles custom error for missing token', async () => { mockSplunkOnCallApi.getUsers = jest .fn() @@ -151,6 +201,20 @@ describe('SplunkOnCallCard', () => { ).toBeInTheDocument(); }); + it('handles warning for missing required annotations', async () => { + const { getAllByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getAllByText('Missing Annotation').length).toEqual(1); + }); + it('handles warning for incorrect team annotation', async () => { mockSplunkOnCallApi.getUsers = jest .fn() @@ -170,7 +234,37 @@ describe('SplunkOnCallCard', () => { ); await waitFor(() => !queryByTestId('progress')); expect( - getByText('Could not find team named "test" in the Splunk On-Call API'), + getByText( + 'Splunk On-Call API returned no record of teams associated with the "test" team name', + ), + ).toBeInTheDocument(); + }); + + it('handles warning for incorrect routing key annotation', async () => { + mockSplunkOnCallApi.getUsers = jest + .fn() + .mockImplementationOnce(async () => [MOCKED_USER]); + mockSplunkOnCallApi.getRoutingKeys = jest + .fn() + .mockImplementationOnce(async () => [MOCK_ROUTING_KEY]); + mockSplunkOnCallApi.getTeams = jest + .fn() + .mockImplementationOnce(async () => []); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect( + getByText( + `Splunk On-Call API returned no record of teams associated with the "${MOCK_ROUTING_KEY.routingKey}" routing key`, + ), ).toBeInTheDocument(); }); diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx index bca53a3b0a..4a6f983636 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.tsx @@ -22,6 +22,7 @@ import { CardContent, CardHeader, Divider, + makeStyles, Typography, } from '@material-ui/core'; import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; @@ -32,8 +33,7 @@ import { MissingApiKeyOrApiIdError } from './Errors/MissingApiKeyOrApiIdError'; import { EscalationPolicy } from './Escalation'; import { Incidents } from './Incident'; import { TriggerDialog } from './TriggerDialog'; -import { User } from './types'; - +import { Team, User } from './types'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import { @@ -45,20 +45,49 @@ import { } from '@backstage/core-components'; export const SPLUNK_ON_CALL_TEAM = 'splunk.com/on-call-team'; +export const SPLUNK_ON_CALL_ROUTING_KEY = 'splunk.com/on-call-routing-key'; -export const MissingTeamAnnotation = () => ( - +export const MissingAnnotation = () => ( +
+ + The Splunk On Call plugin requires setting either the{' '} + {SPLUNK_ON_CALL_TEAM} or the{' '} + {SPLUNK_ON_CALL_ROUTING_KEY} annotation. + + +
); -export const InvalidTeamAnnotation = ({ teamName }: { teamName: string }) => ( - - - -); +export const InvalidAnnotation = ({ + teamName, + routingKey, +}: { + teamName: string | undefined; + routingKey: string | undefined; +}) => { + let titleSuffix = 'provided annotation'; + + if (routingKey) { + titleSuffix = `"${routingKey}" routing key`; + } + + if (teamName) { + titleSuffix = `"${teamName}" team name`; + } + + return ( + + + + + + + ); +}; export const MissingEventsRestEndpoint = () => ( @@ -71,15 +100,28 @@ export const MissingEventsRestEndpoint = () => ( ); export const isSplunkOnCallAvailable = (entity: Entity) => - Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]); + Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]) || + Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_ROUTING_KEY]); + +const useStyles = makeStyles({ + onCallCard: { + marginBottom: '1em', + }, +}); export const EntitySplunkOnCallCard = () => { + const classes = useStyles(); const config = useApi(configApiRef); const api = useApi(splunkOnCallApiRef); const { entity } = useEntity(); const [showDialog, setShowDialog] = useState(false); const [refreshIncidents, setRefreshIncidents] = useState(false); - const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM]; + const teamAnnotation = entity + ? entity.metadata.annotations![SPLUNK_ON_CALL_TEAM] + : undefined; + const routingKeyAnnotation = entity + ? entity.metadata.annotations![SPLUNK_ON_CALL_ROUTING_KEY] + : undefined; const eventsRestEndpoint = config.getOptionalString('splunkOnCall.eventsRestEndpoint') || null; @@ -93,7 +135,7 @@ export const EntitySplunkOnCallCard = () => { }, []); const { - value: usersAndTeam, + value: usersAndTeams, loading, error, } = useAsync(async () => { @@ -108,10 +150,38 @@ export const EntitySplunkOnCallCard = () => { {}, ); const teams = await api.getTeams(); - const foundTeam = teams.find(teamValue => teamValue.name === team); - return { usersHashMap, foundTeam }; + let foundTeams = [ + teams.find(teamValue => teamValue.name === teamAnnotation), + ].filter(team => team !== undefined); + + if (!foundTeams.length && routingKeyAnnotation) { + const routingKeys = await api.getRoutingKeys(); + const foundRoutingKey = routingKeys.find( + key => key.routingKey === routingKeyAnnotation, + ); + foundTeams = foundRoutingKey + ? foundRoutingKey.targets + .map(target => { + const teamUrlParts = target._teamUrl.split('/'); + const teamSlug = teamUrlParts[teamUrlParts.length - 1]; + + return teams.find(teamValue => teamValue.slug === teamSlug); + }) + .filter(team => team !== undefined) + : []; + } + + return { usersHashMap, foundTeams }; }); + if (!teamAnnotation && !routingKeyAnnotation) { + return ; + } + + if (!eventsRestEndpoint) { + return ; + } + if (error instanceof UnauthorizedError) { return ; } @@ -128,27 +198,32 @@ export const EntitySplunkOnCallCard = () => { return ; } - const Content = () => { - if (!team) { - return ; - } + if (!usersAndTeams?.foundTeams || !usersAndTeams?.foundTeams.length) { + return ( + + ); + } - if (!usersAndTeam?.foundTeam) { - return ; - } - - if (!eventsRestEndpoint) { - return ; - } + const Content = ({ + team, + usersHashMap, + }: { + team: Team | undefined; + usersHashMap: any; + }) => { + const teamName = team?.name ?? ''; return ( <> - - {usersAndTeam?.usersHashMap && team && ( - + + {usersHashMap && team && ( + )} { icon: , }; + const teams = usersAndTeams?.foundTeams || []; + return ( - - Team: {team}, - , - ]} - /> - - - - - + <> + {teams.map((team, i) => ( + + + Team: {team && team.name ? team.name : ''} + , + , + ]} + /> + + + + + + ))} + ); }; diff --git a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx index 570b47483f..4c9975bd06 100644 --- a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx +++ b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx @@ -106,7 +106,7 @@ const IncidentAction = ({ switch (currentPhase) { case 'UNACKED': return ( - + acknowledgeAction({ incidentId, incidentType: 'ACKNOWLEDGEMENT' }) diff --git a/plugins/splunk-on-call/src/components/types.ts b/plugins/splunk-on-call/src/components/types.ts index 3c1902e52b..0feefe8b03 100644 --- a/plugins/splunk-on-call/src/components/types.ts +++ b/plugins/splunk-on-call/src/components/types.ts @@ -117,3 +117,15 @@ export type EscalationPolicyTeam = { name: string; slug: string; }; + +export type RoutingKey = { + routingKey: string; + targets: RoutingKeyTarget[]; + isDefault: boolean; +}; + +export type RoutingKeyTarget = { + policyName: string; + policySlug: string; + _teamUrl: string; +}; diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md index 9346b5c402..3128854594 100644 --- a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-tech-insights-backend-module-jsonfc +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + - @backstage/plugin-tech-insights-node@0.2.2 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + - @backstage/plugin-tech-insights-node@0.2.2-next.0 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + - @backstage/plugin-tech-insights-node@0.2.1 + +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + - @backstage/plugin-tech-insights-node@0.2.1-next.0 + ## 0.1.6 ### Patch Changes diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index 66f13b7d1f..fc2c6b7add 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", - "version": "0.1.6", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,11 +31,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", "@backstage/plugin-tech-insights-common": "^0.2.1", - "@backstage/plugin-tech-insights-node": "^0.2.0", + "@backstage/plugin-tech-insights-node": "^0.2.2", "ajv": "^7.0.3", "json-rules-engine": "^6.1.2", "lodash": "^4.17.21", @@ -43,7 +43,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/node-cron": "^3.0.1" }, "files": [ diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index 7cf31f5b61..36e7aa97a1 100644 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -1,5 +1,46 @@ # @backstage/plugin-tech-insights-backend +## 0.2.4 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/backend-common@0.10.7 + - @backstage/plugin-tech-insights-node@0.2.2 + +## 0.2.4-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + - @backstage/plugin-tech-insights-node@0.2.2-next.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + - @backstage/plugin-tech-insights-node@0.2.1 + +## 0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + - @backstage/plugin-tech-insights-node@0.2.1-next.0 + ## 0.2.2 ### Patch Changes diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index febaf37dac..5a60e478d8 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend", - "version": "0.2.2", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,17 +31,17 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/catalog-client": "^0.5.5", + "@backstage/backend-common": "^0.10.7", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", "@backstage/plugin-tech-insights-common": "^0.2.1", - "@backstage/plugin-tech-insights-node": "^0.2.0", + "@backstage/plugin-tech-insights-node": "^0.2.2", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^4.1.0", - "knex": "^0.95.1", + "knex": "^1.0.2", "lodash": "^4.17.21", "luxon": "^2.0.2", "node-cron": "^3.0.0", @@ -51,8 +51,8 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-test-utils": "^0.1.15", - "@backstage/cli": "^0.13.0", + "@backstage/backend-test-utils": "^0.1.17", + "@backstage/cli": "^0.13.2", "@types/supertest": "^2.0.8", "@types/node-cron": "^3.0.0", "@types/semver": "^7.3.8", diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts index b99f6a1c02..a51ce0ab83 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityMetadataFactRetriever.test.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { PluginEndpointDiscovery, getVoidLogger, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { CatalogListResponse } from '@backstage/catalog-client'; +import { GetEntitiesResponse } from '@backstage/catalog-client'; import { entityMetadataFactRetriever } from './entityMetadataFactRetriever'; const getEntitiesMock = jest.fn(); @@ -36,7 +36,7 @@ const discovery: jest.Mocked = { getExternalBaseUrl: jest.fn(), }; -const defaultEntityListResponse: CatalogListResponse = { +const defaultEntityListResponse: GetEntitiesResponse = { items: [ { apiVersion: 'backstage.io/v1beta1', diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts index ea0801f17c..2108379f06 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/entityOwnershipFactRetriever.test.ts @@ -15,13 +15,13 @@ */ import { entityOwnershipFactRetriever } from './entityOwnershipFactRetriever'; -import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { PluginEndpointDiscovery, getVoidLogger, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { CatalogListResponse } from '@backstage/catalog-client'; +import { GetEntitiesResponse } from '@backstage/catalog-client'; const getEntitiesMock = jest.fn(); jest.mock('@backstage/catalog-client', () => { @@ -36,7 +36,7 @@ const discovery: jest.Mocked = { getExternalBaseUrl: jest.fn(), }; -const defaultEntityListResponse: CatalogListResponse = { +const defaultEntityListResponse: GetEntitiesResponse = { items: [ { apiVersion: 'backstage.io/v1beta1', diff --git a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts index 6ed9b12523..a3604129b7 100644 --- a/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts +++ b/plugins/tech-insights-backend/src/service/fact/factRetrievers/techdocsFactRetriever.test.ts @@ -15,13 +15,13 @@ */ import { techdocsFactRetriever } from './techdocsFactRetriever'; -import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { PluginEndpointDiscovery, getVoidLogger, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; -import { CatalogListResponse } from '@backstage/catalog-client'; +import { GetEntitiesResponse } from '@backstage/catalog-client'; const getEntitiesMock = jest.fn(); jest.mock('@backstage/catalog-client', () => { @@ -36,7 +36,7 @@ const discovery: jest.Mocked = { getExternalBaseUrl: jest.fn(), }; -const defaultEntityListResponse: CatalogListResponse = { +const defaultEntityListResponse: GetEntitiesResponse = { items: [ { apiVersion: 'backstage.io/v1beta1', diff --git a/plugins/tech-insights-common/package.json b/plugins/tech-insights-common/package.json index 48ccb0d1a1..a0b840da74 100644 --- a/plugins/tech-insights-common/package.json +++ b/plugins/tech-insights-common/package.json @@ -35,7 +35,7 @@ "@backstage/types": "^0.1.1" }, "devDependencies": { - "@backstage/cli": "^0.13.0" + "@backstage/cli": "^0.13.2-next.0" }, "files": [ "dist" diff --git a/plugins/tech-insights-node/CHANGELOG.md b/plugins/tech-insights-node/CHANGELOG.md index 502cfdaa9b..c38c36ecd9 100644 --- a/plugins/tech-insights-node/CHANGELOG.md +++ b/plugins/tech-insights-node/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-tech-insights-node +## 0.2.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.2.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.2.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index 6bca50cd0a..05c6f10071 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-node", - "version": "0.2.0", + "version": "0.2.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", + "@backstage/backend-common": "^0.10.7", "@backstage/config": "^0.1.13", "@backstage/plugin-tech-insights-common": "^0.2.1", "@types/luxon": "^2.0.5", @@ -38,7 +38,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.0" + "@backstage/cli": "^0.13.2" }, "files": [ "dist" diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md index c760b0c5f5..af16d5f347 100644 --- a/plugins/tech-insights/CHANGELOG.md +++ b/plugins/tech-insights/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/plugin-tech-insights +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.1.6 ### Patch Changes diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index 1d4266777f..b7b1b6877c 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights", - "version": "0.1.6", + "version": "0.1.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/plugin-tech-insights-common": "^0.2.1", "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", @@ -39,16 +39,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 5648323b33..bafe2212cf 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-tech-radar +## 0.5.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.5.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.5.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.5.3 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index cf612f7dce..29601d1755 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-tech-radar", "description": "A Backstage plugin that lets you display a Tech Radar for your organization", - "version": "0.5.3", + "version": "0.5.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -46,10 +46,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", @@ -58,7 +58,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "@types/react": "^16.13.1 || ^17.0.0", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 246d612833..b05e8a6dad 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-techdocs-backend +## 0.13.3 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/backend-common@0.10.7 + - @backstage/techdocs-common@0.11.7 + +## 0.13.3-next.0 + +### Patch Changes + +- 2441d1cf59: chore(deps): bump `knex` from 0.95.6 to 1.0.2 + + This also replaces `sqlite3` with `@vscode/sqlite3` 5.0.7 + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + - @backstage/techdocs-common@0.11.7-next.0 + +## 0.13.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@0.1.2 + - @backstage/backend-common@0.10.6 + - @backstage/techdocs-common@0.11.6 + +## 0.13.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@0.1.2-next.0 + - @backstage/backend-common@0.10.6-next.0 + - @backstage/techdocs-common@0.11.6-next.0 + ## 0.13.1 ### Patch Changes diff --git a/plugins/techdocs-backend/examples/documented-component/docs/extensions.md b/plugins/techdocs-backend/examples/documented-component/docs/extensions.md index e9337ce042..0fda26302d 100644 --- a/plugins/techdocs-backend/examples/documented-component/docs/extensions.md +++ b/plugins/techdocs-backend/examples/documented-component/docs/extensions.md @@ -88,6 +88,12 @@ Weather: :sunny: :umbrella: :cloud: :snowflake: Animals: :tiger: :horse: :turtle: :wolf: :frog: +### Attributes + +[A Download Link](./images/backstage-logo-cncf.svg){: download } + +![A Scaled Image](./images/backstage-logo-cncf.svg){: style="width: 100px" } + ### MDX truly sane lists - attributes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index c760783243..37b15ffbe0 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "0.13.1", + "version": "0.13.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,30 +31,29 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/catalog-client": "^0.5.5", + "@backstage/backend-common": "^0.10.7", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", "@backstage/integration": "^0.7.2", - "@backstage/plugin-catalog-common": "^0.1.1-next.0", + "@backstage/plugin-catalog-common": "^0.1.2", "@backstage/search-common": "^0.2.2", - "@backstage/techdocs-common": "^0.11.5", + "@backstage/techdocs-common": "^0.11.7", "@types/express": "^4.17.6", - "cross-fetch": "^3.0.6", "dockerode": "^3.3.1", "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", - "knex": "^0.95.1", + "knex": "^1.0.2", "lodash": "^4.17.21", - "node-fetch": "^2.6.1", + "node-fetch": "^2.6.7", "p-limit": "^3.1.0", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/test-utils": "^0.2.4", "@types/dockerode": "^3.3.0", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts index 7e7545ea1f..e81fc1e7a9 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.test.ts @@ -198,8 +198,8 @@ describe('DefaultTechDocsCollator', () => { componentType: entity!.spec!.type, lifecycle: entity!.spec!.lifecycle, owner: '', - kind: entity.kind, - name: entity.metadata.name, + kind: entity.kind.toLocaleLowerCase('en-US'), + name: entity.metadata.name.toLocaleLowerCase('en-US'), authorization: { resourceRef: `component:default/${entity.metadata.name}`, }, diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts index 5149fe6c09..987e8c6b15 100644 --- a/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts +++ b/plugins/techdocs-backend/src/search/DefaultTechDocsCollator.ts @@ -145,9 +145,7 @@ export class DefaultTechDocsCollator implements DocumentCollator { path: doc.location, }), path: doc.location, - kind: entity.kind, - namespace: entity.metadata.namespace || 'default', - name: entity.metadata.name, + ...entityInfo, entityTitle: entity.metadata.title, componentType: entity.spec?.type?.toString() || 'other', lifecycle: (entity.spec?.lifecycle as string) || '', diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts index 1eb8c8f56f..1830dbfcf0 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts @@ -31,7 +31,7 @@ import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; jest.mock('../DocsBuilder'); -jest.mock('cross-fetch', () => ({ +jest.mock('node-fetch', () => ({ __esModule: true, default: async () => { return { diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts index 4d407ab54a..099eec152b 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts @@ -24,7 +24,7 @@ import { PreparerBuilder, PublisherBase, } from '@backstage/techdocs-common'; -import fetch from 'cross-fetch'; +import fetch from 'node-fetch'; import { PassThrough } from 'stream'; import * as winston from 'winston'; import { TechDocsCache } from '../cache'; diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 2796548251..3ff8c0e175 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,63 @@ # @backstage/plugin-techdocs +## 0.13.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-search@0.6.2 + - @backstage/plugin-catalog-react@0.6.14 + - @backstage/plugin-catalog@0.7.12 + - @backstage/integration-react@0.1.21 + +## 0.13.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-search@0.6.2-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + - @backstage/integration-react@0.1.21-next.0 + - @backstage/plugin-catalog@0.7.12-next.0 + +## 0.13.2 + +### Patch Changes + +- 742434a6ba: Fixed a bug where links to files within a TechDocs site that use the `download` attribute would result in a 404 in cases where the TechDocs backend and Backstage frontend application are on the same host. +- 359c31e31d: Added support for documentation using the raw `` tag to point to relative resources like audio or video files. +- 18317a08db: Fixed a bug where copy-to-clipboard buttons were appended to unintended elements. +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + - @backstage/integration-react@0.1.20 + - @backstage/plugin-catalog@0.7.11 + - @backstage/plugin-search@0.6.1 + +## 0.13.2-next.1 + +### Patch Changes + +- 742434a6ba: Fixed a bug where links to files within a TechDocs site that use the `download` attribute would result in a 404 in cases where the TechDocs backend and Backstage frontend application are on the same host. +- Updated dependencies + - @backstage/core-components@0.8.7-next.1 + - @backstage/plugin-catalog-react@0.6.13-next.1 + - @backstage/plugin-catalog@0.7.11-next.1 + +## 0.13.2-next.0 + +### Patch Changes + +- 359c31e31d: Added support for documentation using the raw `` tag to point to relative resources like audio or video files. +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/integration-react@0.1.20-next.0 + - @backstage/plugin-catalog@0.7.11-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + - @backstage/plugin-search@0.6.1-next.0 + ## 0.13.1 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 783ad7a5df..794e386136 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "0.13.1", + "version": "0.13.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -34,14 +34,14 @@ "dependencies": { "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", "@backstage/integration": "^0.7.2", - "@backstage/integration-react": "^0.1.19", - "@backstage/plugin-catalog": "^0.7.10", - "@backstage/plugin-catalog-react": "^0.6.12", - "@backstage/plugin-search": "^0.6.0", + "@backstage/integration-react": "^0.1.21", + "@backstage/plugin-catalog": "^0.7.12", + "@backstage/plugin-catalog-react": "^0.6.14", + "@backstage/plugin-search": "^0.6.2", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -62,10 +62,10 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/react-hooks": "^7.0.2", @@ -74,7 +74,7 @@ "@types/jest": "^26.0.7", "@types/node": "^14.14.32", "canvas": "^2.6.1", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 7869fc1b9b..61617ba9ec 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -142,6 +142,7 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { const techdocsSanitizer = useApi(configApiRef); const { namespace = '', kind = '', name = '' } = entityRef; const { state, path, content: rawPage } = useTechDocsReader(); + const isDarkTheme = theme.palette.type === 'dark'; const [sidebars, setSidebars] = useState(); const [dom, setDom] = useState(null); @@ -219,6 +220,27 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { --md-accent-fg-color: ${theme.palette.primary.main}; --md-default-fg-color--lightest: ${theme.palette.textVerySubtle}; } + .codehilite .gd, .highlight .gd { + background-color: ${isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd'}; + } + .codehilite .gi, .highlight .gi { + background-color: ${isDarkTheme ? 'rgba(46,160,67,0.65)' : '#dfd'}; + } + .highlight .kd { + color: ${isDarkTheme ? '#4aaaf7' : '#3b78e7'}; + } + .highlight .k { + color: ${isDarkTheme ? '#4aaaf7' : '#3b78e7'}; + } + .highlight .nx { + color: ${isDarkTheme ? '#ff53a3' : '#ec407a'}; + } + .highlight .s1 { + color: ${isDarkTheme ? '#1cad46' : 'rgb(13, 144, 79)'}; + } + .highlight .kt { + color: ${isDarkTheme ? '#4aaaf7' : '#3e61a2'}; + } .md-main__inner { margin-top: 0; } .md-sidebar { position: fixed; bottom: 100px; width: 20rem; } .md-sidebar--secondary { right: 2rem; } @@ -351,21 +373,22 @@ export const useTechDocsReaderDom = (entityRef: EntityName): Element | null => { }), ]), [ + techdocsSanitizer, + techdocsStorageApi, kind, name, namespace, scmIntegrationsApi, - techdocsStorageApi, - techdocsSanitizer, - theme.palette.action.disabledBackground, - theme.palette.background.default, - theme.palette.background.paper, - theme.palette.primary.main, - theme.palette.success.main, - theme.palette.text.primary, - theme.palette.textSubtle, - theme.palette.textVerySubtle, theme.typography.fontFamily, + theme.palette.text.primary, + theme.palette.primary.main, + theme.palette.background.paper, + theme.palette.background.default, + theme.palette.textVerySubtle, + theme.palette.textSubtle, + theme.palette.action.disabledBackground, + theme.palette.success.main, + isDarkTheme, isPinned, ], ); diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts index 11e8375420..0fb72caab3 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts @@ -42,6 +42,10 @@ const fixture = ` Download Now + `; @@ -88,12 +92,18 @@ describe('addBaseUrl', () => { ); expect(techdocsStorageApi.getBaseUrl).toHaveBeenNthCalledWith( 3, - 'astyle.css', + 'avideo.mp4', mockEntityId, '', ); expect(techdocsStorageApi.getBaseUrl).toHaveBeenNthCalledWith( 4, + 'astyle.css', + mockEntityId, + '', + ); + expect(techdocsStorageApi.getBaseUrl).toHaveBeenNthCalledWith( + 5, 'afile.pdf', mockEntityId, '', diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts index 8f08ec83a6..f3906b7b91 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts @@ -84,6 +84,7 @@ export const addBaseUrl = ({ await Promise.all([ updateDom(dom.querySelectorAll('img'), 'src'), updateDom(dom.querySelectorAll('script'), 'src'), + updateDom(dom.querySelectorAll('source'), 'src'), updateDom(dom.querySelectorAll('link'), 'href'), updateDom(dom.querySelectorAll('a[download]'), 'href'), ]); diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts index fe3a6e557c..f079750d8a 100644 --- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.test.ts @@ -71,4 +71,31 @@ describe('addLinkClickListener', () => { expect(fn).toHaveBeenCalledTimes(0); }); + + it('does not call onClick when a link has a download attribute', async () => { + const fn = jest.fn(); + const shadowDom = await createTestShadowDom( + ` + + + + Download + + + `, + { + preTransformers: [], + postTransformers: [ + addLinkClickListener({ + baseUrl: 'http://localhost:3000', + onClick: fn, + }), + ], + }, + ); + + shadowDom.querySelector('a')?.click(); + + expect(fn).toHaveBeenCalledTimes(0); + }); }); diff --git a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts index f02d88793b..3d0bd137c0 100644 --- a/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts +++ b/plugins/techdocs/src/reader/transformers/addLinkClickListener.ts @@ -32,7 +32,7 @@ export const addLinkClickListener = ({ const href = target.getAttribute('href'); if (!href) return; - if (href.startsWith(baseUrl)) { + if (href.startsWith(baseUrl) && !elem.hasAttribute('download')) { e.preventDefault(); onClick(e, href); } diff --git a/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts b/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts index 679121d9bd..daa074f6e4 100644 --- a/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts +++ b/plugins/techdocs/src/reader/transformers/copyToClipboard.test.ts @@ -32,7 +32,7 @@ describe('copyToClipboard', () => { - ${expectedClipboard} +
${expectedClipboard}
`, @@ -46,4 +46,25 @@ describe('copyToClipboard', () => { expect(clipboardSpy).toHaveBeenCalledWith(expectedClipboard); }); + + it('only gets applied to code blocks', async () => { + const expectedClipboard = 'function foo() {return "bar";}'; + const shadowDom = await createTestShadowDom( + ` + + + + ${expectedClipboard} + + + `, + { + preTransformers: [], + postTransformers: [copyToClipboard()], + }, + ); + + const copyButton = shadowDom.querySelector('button'); + expect(copyButton).toBe(null); + }); }); diff --git a/plugins/techdocs/src/reader/transformers/copyToClipboard.ts b/plugins/techdocs/src/reader/transformers/copyToClipboard.ts index 24b46c0b85..d0569c0fd9 100644 --- a/plugins/techdocs/src/reader/transformers/copyToClipboard.ts +++ b/plugins/techdocs/src/reader/transformers/copyToClipboard.ts @@ -22,7 +22,7 @@ import type { Transformer } from './transformer'; */ export const copyToClipboard = (): Transformer => { return dom => { - Array.from(dom.querySelectorAll('code')).forEach(codeElem => { + Array.from(dom.querySelectorAll('pre > code')).forEach(codeElem => { const button = document.createElement('button'); const toBeCopied = codeElem.textContent || ''; button.className = 'md-clipboard md-icon'; diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index ddbc5d215a..a5cb799a70 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-todo-backend +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@0.6.0 + - @backstage/backend-common@0.10.7 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.7-next.0 + +## 0.1.20 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6 + +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.10.6-next.0 + ## 0.1.19 ### Patch Changes diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 3a8eb1dcac..3371dbbecc 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo-backend", "description": "A Backstage backend plugin that lets you browse TODO comments in your source code", - "version": "0.1.19", + "version": "0.1.21", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,8 +25,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.10.5", - "@backstage/catalog-client": "^0.5.5", + "@backstage/backend-common": "^0.10.7", + "@backstage/catalog-client": "^0.6.0", "@backstage/catalog-model": "^0.9.10", "@backstage/config": "^0.1.13", "@backstage/errors": "^0.2.0", @@ -39,7 +39,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", + "@backstage/cli": "^0.13.2", "@types/supertest": "^2.0.8", "msw": "^0.35.0", "supertest": "^6.1.3" diff --git a/plugins/todo-backend/src/service/TodoReaderService.test.ts b/plugins/todo-backend/src/service/TodoReaderService.test.ts index 3d86a1726b..ce1c61d8a5 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.test.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.test.ts @@ -301,7 +301,7 @@ describe('TodoReaderService', () => { await expect(service.listTodos({ entity: entityName })).rejects.toEqual( expect.objectContaining({ name: 'NotFoundError', - message: 'Entity not found, Component:default/my-component', + message: 'Entity not found, component:default/my-component', }), ); expect(catalogClient.getEntityByName).toHaveBeenCalledWith(entityName, { @@ -321,7 +321,7 @@ describe('TodoReaderService', () => { expect.objectContaining({ name: 'InputError', message: - 'No entity location annotation found for Component:my-component', + 'No entity location annotation found for component:default/my-component', }), ); }); @@ -342,7 +342,7 @@ describe('TodoReaderService', () => { await expect(service.listTodos({ entity: entityName })).rejects.toEqual( expect.objectContaining({ name: 'InputError', - message: `Invalid entity location type for Component:my-component, got 'file'`, + message: `Invalid entity location type for component:default/my-component, got 'file'`, }), ); }); @@ -363,7 +363,7 @@ describe('TodoReaderService', () => { await expect(service.listTodos({ entity: entityName })).rejects.toEqual( expect.objectContaining({ name: 'InputError', - message: `Invalid entity source location type for Component:my-component, got 'file'`, + message: `Invalid entity source location type for component:default/my-component, got 'file'`, }), ); }); diff --git a/plugins/todo-backend/src/service/TodoReaderService.ts b/plugins/todo-backend/src/service/TodoReaderService.ts index 54957d89c4..86f53057da 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.ts @@ -17,11 +17,11 @@ import { InputError, NotFoundError } from '@backstage/errors'; import { CatalogApi } from '@backstage/catalog-client'; import { - LOCATION_ANNOTATION, - SOURCE_LOCATION_ANNOTATION, - serializeEntityRef, + ANNOTATION_LOCATION, + ANNOTATION_SOURCE_LOCATION, Entity, - parseLocationReference, + parseLocationRef, + stringifyEntityRef, } from '@backstage/catalog-model'; import { TodoReader } from '../lib'; import { ListTodosRequest, ListTodosResponse, TodoService } from './types'; @@ -71,7 +71,7 @@ export class TodoReaderService implements TodoService { }); if (!entity) { throw new NotFoundError( - `Entity not found, ${serializeEntityRef(req.entity)}`, + `Entity not found, ${stringifyEntityRef(req.entity)}`, ); } @@ -128,12 +128,12 @@ export class TodoReaderService implements TodoService { private getEntitySourceUrl(entity: Entity) { const sourceLocation = - entity.metadata.annotations?.[SOURCE_LOCATION_ANNOTATION]; + entity.metadata.annotations?.[ANNOTATION_SOURCE_LOCATION]; if (sourceLocation) { - const parsed = parseLocationReference(sourceLocation); + const parsed = parseLocationRef(sourceLocation); if (parsed.type !== 'url') { throw new InputError( - `Invalid entity source location type for ${serializeEntityRef( + `Invalid entity source location type for ${stringifyEntityRef( entity, )}, got '${parsed.type}'`, ); @@ -141,12 +141,12 @@ export class TodoReaderService implements TodoService { return parsed.target; } - const location = entity.metadata.annotations?.[LOCATION_ANNOTATION]; + const location = entity.metadata.annotations?.[ANNOTATION_LOCATION]; if (location) { - const parsed = parseLocationReference(location); + const parsed = parseLocationRef(location); if (parsed.type !== 'url') { throw new InputError( - `Invalid entity location type for ${serializeEntityRef( + `Invalid entity location type for ${stringifyEntityRef( entity, )}, got '${parsed.type}'`, ); @@ -154,7 +154,7 @@ export class TodoReaderService implements TodoService { return parsed.target; } throw new InputError( - `No entity location annotation found for ${serializeEntityRef(entity)}`, + `No entity location annotation found for ${stringifyEntityRef(entity)}`, ); } } diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md index d3679e2fd9..bec930df20 100644 --- a/plugins/todo/CHANGELOG.md +++ b/plugins/todo/CHANGELOG.md @@ -1,5 +1,45 @@ # @backstage/plugin-todo +## 0.2.0 + +### Minor Changes + +- 323f48704d: **BREAKING**: The `EntityTodoContent` is now a routable extension. This means it must be rendered within a route, but that's most likely already the case for most apps. The mount point `RouteRef` is available via `todoPlugin.routes.entityContent`. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + - @backstage/plugin-catalog-react@0.6.14 + +## 0.2.0-next.0 + +### Minor Changes + +- 323f48704d: **BREAKING**: The `EntityTodoContent` is now a routable extension. This means it must be rendered within a route, but that's most likely already the case for most apps. The mount point `RouteRef` is available via `todoPlugin.routes.entityContent`. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + - @backstage/plugin-catalog-react@0.6.14-next.0 + +## 0.1.21 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + - @backstage/plugin-catalog-react@0.6.13 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + - @backstage/plugin-catalog-react@0.6.13-next.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/todo/api-report.md b/plugins/todo/api-report.md index 0784f5d56e..c1667f1111 100644 --- a/plugins/todo/api-report.md +++ b/plugins/todo/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 { IdentityApi } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; // @public export const EntityTodoContent: () => JSX.Element; @@ -79,5 +80,10 @@ export type TodoListResult = { }; // @public -export const todoPlugin: BackstagePlugin<{}, {}>; +export const todoPlugin: BackstagePlugin< + { + entityContent: RouteRef; + }, + {} +>; ``` diff --git a/plugins/todo/dev/index.tsx b/plugins/todo/dev/index.tsx index fe547812ec..375c89e4a4 100644 --- a/plugins/todo/dev/index.tsx +++ b/plugins/todo/dev/index.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Entity, LOCATION_ANNOTATION } from '@backstage/catalog-model'; +import { Entity, ANNOTATION_LOCATION } from '@backstage/catalog-model'; import { createDevApp } from '@backstage/dev-utils'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import OnlineIcon from '@material-ui/icons/Cloud'; @@ -31,7 +31,7 @@ const entity: Entity = { metadata: { name: 'backstage', annotations: { - [LOCATION_ANNOTATION]: + [ANNOTATION_LOCATION]: 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', }, }, diff --git a/plugins/todo/package.json b/plugins/todo/package.json index c2fa299175..8282772d90 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo", "description": "A Backstage plugin that lets you browse TODO comments in your source code", - "version": "0.1.20", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,10 +28,10 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.10", - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", - "@backstage/plugin-catalog-react": "^0.6.12", + "@backstage/plugin-catalog-react": "^0.6.14", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -42,16 +42,17 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", + "react-router": "6.0.0-beta.0", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/todo/src/plugin.test.tsx b/plugins/todo/src/plugin.test.tsx new file mode 100644 index 0000000000..57b9be291d --- /dev/null +++ b/plugins/todo/src/plugin.test.tsx @@ -0,0 +1,56 @@ +/* + * 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 React from 'react'; +import { Route } from 'react-router'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { todoPlugin, EntityTodoContent } from './plugin'; +import { todoApiRef } from './api'; + +describe('todo', () => { + it('should export plugin', () => { + expect(todoPlugin).toBeDefined(); + }); + + it('should render EntityTodoContent', async () => { + const rendered = await renderInTestApp( + ({ + items: [ + { + tag: 'FIXME', + text: 'Make sure this test works', + }, + ], + limit: 10, + offset: 0, + totalCount: 1, + }), + }, + ], + ]} + > + } /> + , + ); + + await expect(rendered.findByText('FIXME')).resolves.toBeInTheDocument(); + }); +}); diff --git a/plugins/todo/src/plugin.ts b/plugins/todo/src/plugin.ts index a7d3702f2c..d74acfb02c 100644 --- a/plugins/todo/src/plugin.ts +++ b/plugins/todo/src/plugin.ts @@ -17,10 +17,11 @@ import { todoApiRef, TodoClient } from './api'; import { createApiFactory, createPlugin, - createComponentExtension, + createRoutableExtension, discoveryApiRef, identityApiRef, } from '@backstage/core-plugin-api'; +import { rootRouteRef } from './routes'; /** * The Todo plugin instance. @@ -42,7 +43,7 @@ export const todoPlugin = createPlugin({ }), ], routes: { - // root: rootRouteRef, + entityContent: rootRouteRef, }, }); @@ -52,12 +53,9 @@ export const todoPlugin = createPlugin({ * @public */ export const EntityTodoContent = todoPlugin.provide( - createComponentExtension({ + createRoutableExtension({ name: 'EntityTodoContent', - component: { - lazy: () => import('./components/TodoList').then(m => m.TodoList), - }, - // TODO(Rugvip): Switch back to routable extension once apps are migrated - // mountPoint: rootRouteRef, + component: () => import('./components/TodoList').then(m => m.TodoList), + mountPoint: rootRouteRef, }), ); diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 091a12c728..423e978352 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-user-settings +## 0.3.19 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.3.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.3.18 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.3.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.3.17 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 94987cf96d..4c33a1ac50 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings", "description": "A Backstage plugin that provides a settings page", - "version": "0.3.17", + "version": "0.3.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -44,16 +44,16 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index 632947f505..bcfbf2b88b 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-xcmetrics +## 0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8 + +## 0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.8-next.0 + +## 0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7 + +## 0.2.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.8.7-next.0 + ## 0.2.16 ### Patch Changes diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index bd994dfc28..b765a1be2d 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-xcmetrics", "description": "A Backstage plugin that shows XCode build metrics for your components", - "version": "0.2.16", + "version": "0.2.18", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,7 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.6", + "@backstage/core-components": "^0.8.8", "@backstage/core-plugin-api": "^0.6.0", "@backstage/errors": "^0.2.0", "@backstage/theme": "^0.2.14", @@ -37,17 +37,17 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.13.0", - "@backstage/core-app-api": "^0.5.1", - "@backstage/dev-utils": "^0.2.19", - "@backstage/test-utils": "^0.2.3", + "@backstage/cli": "^0.13.2", + "@backstage/core-app-api": "^0.5.2", + "@backstage/dev-utils": "^0.2.21", + "@backstage/test-utils": "^0.2.4", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", - "cross-fetch": "^3.0.6", + "cross-fetch": "^3.1.5", "msw": "^0.35.0" }, "files": [ diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index bfb8b525fc..2958b5b719 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -193,7 +193,7 @@ const SKIPPED_PACKAGES = [ join('packages', 'codemods'), join('packages', 'create-app'), join('packages', 'e2e-test'), - join('packages', 'embedded-techdocs-app'), + join('packages', 'techdocs-cli-embedded-app'), join('packages', 'storybook'), join('packages', 'techdocs-cli'), ]; @@ -216,9 +216,13 @@ const NO_WARNING_PACKAGES = [ 'packages/test-utils', 'packages/theme', 'packages/types', + 'packages/release-manifests', 'packages/version-bridge', + 'plugins/auth-node', 'plugins/catalog-backend-module-ldap', + 'plugins/catalog-backend-module-msgraph', 'plugins/catalog-common', + 'plugins/catalog-graph', 'plugins/permission-backend', 'plugins/permission-common', 'plugins/permission-node', @@ -573,6 +577,216 @@ forking rushstash to modify the api-documenter markdown generation, which otherwise is the recommended way to do customizations. */ +type ExcerptToken = { + kind: string; + text: string; + canonicalReference?: string; +}; + +class ExcerptTokenMatcher { + readonly #tokens: ExcerptToken[]; + + constructor(tokens: ExcerptToken[]) { + this.#tokens = tokens.slice(); + } + + nextContent() { + const token = this.#tokens.shift(); + if (token?.kind === 'Content') { + return token.text; + } + return undefined; + } + + matchContent(expectedText: string) { + const text = this.nextContent(); + return text !== expectedText; + } + + getTokensUntilArrow() { + const tokens = []; + for (;;) { + const token = this.#tokens.shift(); + if (token === undefined) { + return undefined; + } + if (token.kind === 'Content' && token.text === ') => ') { + return tokens; + } + tokens.push(token); + } + } + + getComponentReturnTokens() { + const first = this.#tokens.shift(); + if (!first) { + return undefined; + } + const second = this.#tokens.shift(); + + if (this.#tokens.length !== 0) { + return undefined; + } + if (first.kind !== 'Reference' || first.text !== 'JSX.Element') { + return undefined; + } + if (!second) { + return [first]; + } else if (second.kind === 'Content' && second.text === ' | null') { + return [first, second]; + } + return undefined; + } +} + +class ApiModelTransforms { + static deserializeWithTransforms( + serialized: any, + transforms: Array<(member: any) => any>, + ): ApiPackage { + if (serialized.kind !== 'Package') { + throw new Error( + `Unexpected root kind in serialized ApiPackage, ${serialized.kind}`, + ); + } + if (serialized.members.length !== 1) { + throw new Error( + `Unexpected members in serialized ApiPackage, [${serialized.members + .map(m => m.kind) + .join(' ')}]`, + ); + } + const [entryPoint] = serialized.members; + if (entryPoint.kind !== 'EntryPoint') { + throw new Error( + `Unexpected kind in serialized ApiPackage member, ${entryPoint.kind}`, + ); + } + + const transformed = { + ...serialized, + members: [ + { + ...entryPoint, + members: entryPoint.members.map(member => + transforms.reduce((m, t) => t(m), member), + ), + }, + ], + }; + + return ApiPackage.deserialize( + transformed, + transformed.metadata, + ) as ApiPackage; + } + + static transformArrowComponents = (member: any) => { + if (member.kind !== 'Variable') { + return member; + } + + const { name, excerptTokens } = member; + + // First letter in name must be uppercase + const [firstChar] = name; + if (firstChar.toLocaleUpperCase('en-US') !== firstChar) { + return member; + } + + // First content must match expected declaration format + const tokens = new ExcerptTokenMatcher(excerptTokens); + if (tokens.nextContent() !== `${name}: `) { + return member; + } + + // Next needs to be an arrow with `props` parameters or no parameters + // followed by a return type of `JSX.Element | null` or just `JSX.Element` + const declStart = tokens.nextContent(); + if (declStart === '(props: ' || declStart === '(_props: ') { + const props = tokens.getTokensUntilArrow(); + const ret = tokens.getComponentReturnTokens(); + if (props && ret) { + return this.makeComponentMember(member, ret, props); + } + } else if (declStart === '() => ') { + const ret = tokens.getComponentReturnTokens(); + if (ret) { + return this.makeComponentMember(member, ret); + } + } + return member; + }; + + static makeComponentMember( + member: any, + ret: ExcerptToken[], + props?: ExcerptToken[], + ) { + const declTokens = props + ? [ + { + kind: 'Content', + text: `export declare function ${member.name}(props: `, + }, + ...props, + { + kind: 'Content', + text: '): ', + }, + ] + : [ + { + kind: 'Content', + text: `export declare function ${member.name}(): `, + }, + ]; + + return { + kind: 'Function', + name: member.name, + releaseTag: member.releaseTag, + docComment: member.docComment ?? '', + canonicalReference: member.canonicalReference, + excerptTokens: [...declTokens, ...ret], + returnTypeTokenRange: { + startIndex: declTokens.length, + endIndex: declTokens.length + ret.length, + }, + parameters: props + ? [ + { + parameterName: 'props', + parameterTypeTokenRange: { + startIndex: 1, + endIndex: 1 + props.length, + }, + }, + ] + : [], + overloadIndex: 1, + }; + } + + static transformTrimDeclare = (member: any) => { + const { excerptTokens } = member; + const firstContent = new ExcerptTokenMatcher(excerptTokens).nextContent(); + if (firstContent && firstContent.startsWith('export declare ')) { + return { + ...member, + excerptTokens: [ + { + kind: 'Content', + text: firstContent.slice('export declare '.length), + }, + ...excerptTokens.slice(1), + ], + }; + } + return member; + }; +} + async function buildDocs({ inputDir, outputDir, @@ -596,13 +810,12 @@ async function buildDocs({ const newModel = new ApiModel(); for (const serialized of serializedPackages) { - // Add any docs filtering logic here - - const pkg = ApiPackage.deserialize( - serialized, - serialized.metadata, - ) as ApiPackage; - newModel.addMember(pkg); + newModel.addMember( + ApiModelTransforms.deserializeWithTransforms(serialized, [ + ApiModelTransforms.transformArrowComponents, + ApiModelTransforms.transformTrimDeclare, + ]), + ); } // The doc AST need to be extended with custom nodes if we want to @@ -818,9 +1031,9 @@ async function main() { const selectedPackageDirs = await findSpecificPackageDirs( process.argv.slice(2).filter(arg => !arg.startsWith('--')), ); - if (selectedPackageDirs && (isCiBuild || isDocsBuild)) { + if (selectedPackageDirs && isCiBuild) { throw new Error( - 'Package path arguments are not supported for the --ci and --docs flags', + 'Package path arguments are not supported together with the --ci flag', ); } if (!selectedPackageDirs && !isCiBuild && !isDocsBuild) { diff --git a/scripts/create-github-release.js b/scripts/create-github-release.js index 6150b5f4bd..f25c6838fb 100755 --- a/scripts/create-github-release.js +++ b/scripts/create-github-release.js @@ -142,7 +142,8 @@ async function getReleaseDescriptionFromCommitMessage(commitMessage) { // Use the PR description to prepare for the release description const isChangesetRelease = commitMessage.includes(CHANGESET_RELEASE_BRANCH); if (isChangesetRelease) { - return data.body.split('\n').slice(3).join('\n'); + const lines = data.body.split('\n'); + return lines.slice(lines.indexOf('# Releases') + 1).join('\n'); } return data.body; @@ -182,7 +183,6 @@ async function main() { const releaseDescription = await getReleaseDescriptionFromCommitMessage( commitMessage, ); - await createRelease(releaseDescription); } diff --git a/scripts/create-release-tag.js b/scripts/create-release-tag.js index a71afe513b..637467f285 100755 --- a/scripts/create-release-tag.js +++ b/scripts/create-release-tag.js @@ -17,37 +17,25 @@ */ const { Octokit } = require('@octokit/rest'); +const path = require('path'); +const fs = require('fs-extra'); const baseOptions = { owner: 'backstage', repo: 'backstage', }; -async function main() { - const { GITHUB_SHA, GITHUB_TOKEN } = process.env; - if (!GITHUB_SHA) { - throw new Error('GITHUB_SHA is not set'); - } - if (!GITHUB_TOKEN) { - throw new Error('GITHUB_TOKEN is not set'); - } - - const octokit = new Octokit({ auth: GITHUB_TOKEN }); - - const rootPath = path.resolve(__dirname, '..'); - const { version: currentVersion } = await fs.readJson( - path.join(rootPath, 'package.json'), - ); - - const tagName = `v${currentVersion}`; - - console.log(`Creating release tag ${tagName}`); +async function getCurrentReleaseTag() { + const rootPath = path.resolve(__dirname, '../package.json'); + return fs.readJson(rootPath).then(_ => _.version); +} +async function createGitTag(octokit, commitSha, tagName) { const annotatedTag = await octokit.git.createTag({ ...baseOptions, tag: tagName, message: tagName, - object: GITHUB_SHA, + object: commitSha, type: 'commit', }); @@ -67,8 +55,46 @@ async function main() { console.error(`Tag creation for ${tagName} failed`); throw ex; } +} + +async function dispatchReleaseWorkflows(octokit, releaseVersion) { + console.log('Dispatching release manifest sync'); + await octokit.actions.createWorkflowDispatch({ + owner: 'backstage', + repo: 'backstage', + workflow_id: 'sync_release-manifest.yml', + ref: 'master', + inputs: { + version: releaseVersion, + }, + }); +} + +async function main() { + const shouldDispatch = process.argv.includes('--dispatch-workflows'); + + if (!process.env.GITHUB_SHA) { + throw new Error('GITHUB_SHA is not set'); + } + if (!process.env.GITHUB_TOKEN) { + throw new Error('GITHUB_TOKEN is not set'); + } + + const commitSha = process.env.GITHUB_SHA; + const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); + + const releaseVersion = await getCurrentReleaseTag(); + const tagName = `v${releaseVersion}`; + + console.log(`Creating release tag ${tagName} at ${commitSha}`); + await createGitTag(octokit, commitSha, tagName); console.log(`::set-output name=tag_name::${tagName}`); + + if (shouldDispatch) { + console.log(`Dispatching release workflows for ${tagName}`); + await dispatchReleaseWorkflows(octokit, releaseVersion); + } } main().catch(error => { diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index 7bb3794d9c..8025022cf8 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -25,6 +25,9 @@ const { promisify } = require('util'); const execFile = promisify(execFileCb); +// All of these are considered to be main-line release branches +const MAIN_BRANCHES = ['master', 'origin/master', 'changeset-release/master']; + const DEPENDENCY_TYPES = [ 'dependencies', 'devDependencies', @@ -59,14 +62,22 @@ async function updatePatchVersions() { throw new Error(`Invalid base version ${version} for package ${name}`); } - const currentVersion = semver.parse(pkg.packageJson.version); - const targetVersion = semver.parse(version).inc('patch'); - targetVersion.prerelease = currentVersion.prerelease; + let targetVersion = version; + + // If we're currently in a pre-release we need to manually execute the + // patch bump up to the next version. And we also need to make sure we + // resume the releases at the same pre-release tag. + const currentPrerelease = semver.prerelease(pkg.packageJson.version); + if (currentPrerelease) { + const parsed = targetVersion.parse(version); + parsed.inc('patch'); + parsed.prerelease = currentPrerelease; + targetVersion = parsed.format(); + } - const targetVersionString = targetVersion.format(); pendingVersionBumps.set(name, { - targetVersion: targetVersionString, - targetRange: `^${targetVersionString}`, + targetVersion, + targetRange: `^${targetVersion}`, }); } @@ -147,7 +158,7 @@ async function updateBackstageReleaseVersion() { const { version: currentVersion } = package; let nextVersion; - if (branchName === 'master') { + if (MAIN_BRANCHES.includes(branchName)) { if (preMode === 'pre') { if (semver.prerelease(currentVersion)) { nextVersion = semver.inc(currentVersion, 'pre', preTag); diff --git a/scripts/verify-changesets.js b/scripts/verify-changesets.js index 8d828ee058..9e3064eeac 100755 --- a/scripts/verify-changesets.js +++ b/scripts/verify-changesets.js @@ -26,7 +26,7 @@ const privatePackages = new Set([ 'example-backend', 'e2e-test', 'storybook', - 'embedded-techdocs-app', + 'techdocs-cli-embedded-app', ]); async function main() { diff --git a/storybook/.npmrc b/storybook/.npmrc new file mode 100644 index 0000000000..c3c66347fd --- /dev/null +++ b/storybook/.npmrc @@ -0,0 +1,2 @@ +registry=https://registry.npmjs.org/ +engine-strict=true diff --git a/storybook/.yarnrc b/storybook/.yarnrc new file mode 100644 index 0000000000..2b1d0857dd --- /dev/null +++ b/storybook/.yarnrc @@ -0,0 +1,9 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +registry "https://registry.npmjs.org/" +disable-self-update-check true +lastUpdateCheck 1580389148099 +yarn-path "../.yarn/releases/yarn-1.22.1.js" +network-timeout 300000 diff --git a/storybook/package.json b/storybook/package.json index a58d5df8c2..5a3437f697 100644 --- a/storybook/package.json +++ b/storybook/package.json @@ -15,18 +15,18 @@ "react-dom": "^17.0.2" }, "devDependencies": { - "@storybook/addon-a11y": "^6.4.14", - "@storybook/addon-actions": "^6.4.14", - "@storybook/addon-links": "^6.4.14", - "@storybook/addon-storysource": "^6.4.14", - "@storybook/addons": "^6.4.14", - "@storybook/react": "^6.4.14", + "@storybook/addon-a11y": "^6.4.19", + "@storybook/addon-actions": "^6.4.19", + "@storybook/addon-links": "^6.4.19", + "@storybook/addon-storysource": "^6.4.19", + "@storybook/addons": "^6.4.19", + "@storybook/react": "^6.4.19", "storybook-dark-mode": "^1.0.8" }, "peerDependencies": { - "@backstage/theme": "file:../packages/theme", - "@backstage/test-utils": "file:../packages/test-utils", - "@backstage/core-app-api": "file:../packages/core-app-api", - "@backstage/core-plugin-api": "file:../packages/core-plugin-api" + "@backstage/theme": "link:../packages/theme", + "@backstage/test-utils": "link:../packages/test-utils", + "@backstage/core-app-api": "link:../packages/core-app-api", + "@backstage/core-plugin-api": "link:../packages/core-plugin-api" } } diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 498d2027ce..94f7e109ae 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -1310,19 +1310,19 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== -"@storybook/addon-a11y@^6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.4.14.tgz#e281551850992ad3b6a693b5b7059344763c984e" - integrity sha512-YehZ4AytPX9kduqZ/HByMluXlftQlKGkALXnM48uXlQlN51ZzjtUzvyvqHyow1g3lg/THFuEGvKEiT/ebThhpg== +"@storybook/addon-a11y@^6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.4.19.tgz#fb92840b5559b429afe2f28211440a5b5d1b6fcb" + integrity sha512-dG6easap6W4AqyggVZPq8lBrhza8StA8J4eYz/GVdoXINSGtq/casV0rkmY3+SUXhPYux5oGavHo86j5I4Q/0Q== dependencies: - "@storybook/addons" "6.4.14" - "@storybook/api" "6.4.14" - "@storybook/channels" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/components" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/api" "6.4.19" + "@storybook/channels" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/components" "6.4.19" + "@storybook/core-events" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.14" + "@storybook/theming" "6.4.19" axe-core "^4.2.0" core-js "^3.8.2" global "^4.4.0" @@ -1332,17 +1332,17 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/addon-actions@^6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.4.14.tgz#be5590438943487b4243b2fe16619557cb9ca0ce" - integrity sha512-EBraATDCKCbb1IpT+bTIV+noFIoK5ykXj8Nt0qmQGD2OC1cZovIyH3DigyD0/3D55znGzxqRruTK8lm0nc1jbg== +"@storybook/addon-actions@^6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.4.19.tgz#10631d9c0a6669810264ea7fac3bff7201553084" + integrity sha512-GpSvP8xV8GfNkmtGJjfCgaOx6mbjtyTK0aT9FqX9pU0s+KVMmoCTrBh43b7dWrwxxas01yleBK9VpYggzhi/Fw== dependencies: - "@storybook/addons" "6.4.14" - "@storybook/api" "6.4.14" - "@storybook/components" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/api" "6.4.19" + "@storybook/components" "6.4.19" + "@storybook/core-events" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.14" + "@storybook/theming" "6.4.19" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -1356,16 +1356,16 @@ util-deprecate "^1.0.2" uuid-browser "^3.1.0" -"@storybook/addon-links@^6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-6.4.14.tgz#7dedd5502cf1fecdcfc845e59a10769cb32ec175" - integrity sha512-Y+5tdmAdkFzk0OC9wJnHdpVfhq3uqqlrAUFE3QYeof4uL6wLNSr2pl0BzCGQtnTfLs0i0bExXaTP5pZhwSnQoA== +"@storybook/addon-links@^6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-6.4.19.tgz#001f26c4ffc7d36fd6018b8a137449948b337869" + integrity sha512-ebFHYlGDQkHSmI5QEJb1NxGNToVOLgjKkxXUe+JXX7AfHvrWiXVrN/57aOtBPZzj4h2jRPRTZgwR5glhPIlfEQ== dependencies: - "@storybook/addons" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/core-events" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/router" "6.4.14" + "@storybook/router" "6.4.19" "@types/qs" "^6.9.5" core-js "^3.8.2" global "^4.4.0" @@ -1374,18 +1374,18 @@ regenerator-runtime "^0.13.7" ts-dedent "^2.0.0" -"@storybook/addon-storysource@^6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/addon-storysource/-/addon-storysource-6.4.14.tgz#dd5421b79b43671e5921701b66cd69500d750e8f" - integrity sha512-vifb7M9x65dZq2f5wfQoVBRapfROK5oXS6VsV3xpqs308oD8IL0fP0KrIRYwJIpRxjTWf+C75Lyd2teoDD9S1w== +"@storybook/addon-storysource@^6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/addon-storysource/-/addon-storysource-6.4.19.tgz#09ec47b0c9f68ba9590edfcc7bd72984c048d272" + integrity sha512-WcjPgd0/m9rGMQbf6H2/wvXUhW5lSELWdXeCCK1taQHQUjhs8tq83fa7OkI9kE25drPiUJuPzObbCoYr94hftQ== dependencies: - "@storybook/addons" "6.4.14" - "@storybook/api" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/components" "6.4.14" - "@storybook/router" "6.4.14" - "@storybook/source-loader" "6.4.14" - "@storybook/theming" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/api" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/components" "6.4.19" + "@storybook/router" "6.4.19" + "@storybook/source-loader" "6.4.19" + "@storybook/theming" "6.4.19" core-js "^3.8.2" estraverse "^5.2.0" loader-utils "^2.0.0" @@ -1394,35 +1394,35 @@ react-syntax-highlighter "^13.5.3" regenerator-runtime "^0.13.7" -"@storybook/addons@6.4.14", "@storybook/addons@^6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.4.14.tgz#45d6937bc2ece33ceadc5358b2a2298d2a0d1e95" - integrity sha512-Snu42ejLyBAh6PWdlrdI72HKN1oKY7q0R9qEID2wk953WrqgGu4URakp14YLxghJCyKTSfGPs6LNZRRI6H5xgA== +"@storybook/addons@6.4.19", "@storybook/addons@^6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.4.19.tgz#797d912b8b5a86cd6e0d31fa4c42d1f80808a432" + integrity sha512-QNyRYhpqmHV8oJxxTBdkRlLSbDFhpBvfvMfIrIT1UXb/eemdBZTaCGVvXZ9UixoEEI7f8VwAQ44IvkU5B1509w== dependencies: - "@storybook/api" "6.4.14" - "@storybook/channels" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/api" "6.4.19" + "@storybook/channels" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/core-events" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/router" "6.4.14" - "@storybook/theming" "6.4.14" + "@storybook/router" "6.4.19" + "@storybook/theming" "6.4.19" "@types/webpack-env" "^1.16.0" core-js "^3.8.2" global "^4.4.0" regenerator-runtime "^0.13.7" -"@storybook/api@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/api/-/api-6.4.14.tgz#a477646f7e020a362f044d2e614e3d1a86ba8f6f" - integrity sha512-GGGwB5+EquoausTXYx4dnLBBk2sOiS1Z58mDj0swBXCZdjfyUfLyxjxvvb/hl65ltufWP3IdmlKKaLiuARXNtw== +"@storybook/api@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.4.19.tgz#8000a0e4c52c39b910b4ccc6731419e8e71800ef" + integrity sha512-aDvea+NpQCBjpNp9YidO1Pr7fzzCp15FSdkG+2ihGQfv5raxrN+IIJnGUXecpe71nvlYiB+29UXBVK7AL0j51Q== dependencies: - "@storybook/channels" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/channels" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/core-events" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/router" "6.4.14" + "@storybook/router" "6.4.19" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.4.14" + "@storybook/theming" "6.4.19" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -1434,10 +1434,10 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/builder-webpack4@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.4.14.tgz#77f45d164f5b93776fa154252706c6b73bd0edc5" - integrity sha512-hRzwdNNLxuyb0XPpvbTSkQuqG2frhog2SsjgPVXorsSMPr95owo9Nq9hp+TnywpvaR9lrPlESzhhv2sSR3blTw== +"@storybook/builder-webpack4@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.4.19.tgz#ca8228639be06e50d5f1555b844dd4177e5068ad" + integrity sha512-wxA6SMH11duc9D53aeVVBwrVRemFIoxHp/dOugkkg6ZZFAb4ZmWzf/ENc3vQIZdZpfNRi7IZIZEOfoHc994cmw== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -1460,22 +1460,22 @@ "@babel/preset-env" "^7.12.11" "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" - "@storybook/addons" "6.4.14" - "@storybook/api" "6.4.14" - "@storybook/channel-postmessage" "6.4.14" - "@storybook/channels" "6.4.14" - "@storybook/client-api" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/components" "6.4.14" - "@storybook/core-common" "6.4.14" - "@storybook/core-events" "6.4.14" - "@storybook/node-logger" "6.4.14" - "@storybook/preview-web" "6.4.14" - "@storybook/router" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/api" "6.4.19" + "@storybook/channel-postmessage" "6.4.19" + "@storybook/channels" "6.4.19" + "@storybook/client-api" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/components" "6.4.19" + "@storybook/core-common" "6.4.19" + "@storybook/core-events" "6.4.19" + "@storybook/node-logger" "6.4.19" + "@storybook/preview-web" "6.4.19" + "@storybook/router" "6.4.19" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.14" - "@storybook/theming" "6.4.14" - "@storybook/ui" "6.4.14" + "@storybook/store" "6.4.19" + "@storybook/theming" "6.4.19" + "@storybook/ui" "6.4.19" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" autoprefixer "^9.8.6" @@ -1509,51 +1509,51 @@ webpack-hot-middleware "^2.25.1" webpack-virtual-modules "^0.2.2" -"@storybook/channel-postmessage@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.4.14.tgz#ce719041768ea8c0d64b7edc32ec7c774fba9b19" - integrity sha512-z+fBi/eAAswELWOdlIFI9XXNjyxfguKyqKGSQ7qdz3eFyxeuWnxTa9aZsnLIXpPKY9QPydpBSJcIKUCdN6DbIg== +"@storybook/channel-postmessage@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.4.19.tgz#5db4e1188aaa9de05fee3ba6a6b7f3b988cade03" + integrity sha512-E5h/itFzQ/6M08LR4kqlgqqmeO3tmavI+nUAlZrkCrotpJFNMHE2i0PQHg0TkFJrRDpYcrwD+AjUW4IwdqrisQ== dependencies: - "@storybook/channels" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/channels" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/core-events" "6.4.19" core-js "^3.8.2" global "^4.4.0" qs "^6.10.0" telejson "^5.3.2" -"@storybook/channel-websocket@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/channel-websocket/-/channel-websocket-6.4.14.tgz#d71a4c8a4b36e2e89a4a3c56af3e0aa50353e02f" - integrity sha512-4Y6TDeYLzItGIaYKo3s6xxSmUF11j96dOX7n74ax45zcMhpp/XwG5i0FU1DtGb5PnhPxg+vJmKa1IgizzaWRYg== +"@storybook/channel-websocket@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/channel-websocket/-/channel-websocket-6.4.19.tgz#5b2f34f9089966bab66c55721766d3d1803edf2e" + integrity sha512-cXKwQjIXttfdUyZlcHORelUmJ5nUKswsnCA/qy7IRWpZjD8yQJcNk1dYC+tTHDVqFgdRT89pL0hRRB1rlaaR8Q== dependencies: - "@storybook/channels" "6.4.14" - "@storybook/client-logger" "6.4.14" + "@storybook/channels" "6.4.19" + "@storybook/client-logger" "6.4.19" core-js "^3.8.2" global "^4.4.0" telejson "^5.3.2" -"@storybook/channels@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.4.14.tgz#f7a5416c971febd26ed7b03a75d99fd819790e48" - integrity sha512-3QOVxFG6ZAxDXCta1ie4SUPQ3s50yHeuZzVg6uPp+DcC1FrXeDFYBcU9t0j/jrSgbeKcnFHWxmRHNy1BRyWv/A== +"@storybook/channels@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.4.19.tgz#095bbaee494bf5b03f7cb92d34626f2f5063cb31" + integrity sha512-EwyoncFvTfmIlfsy8jTfayCxo2XchPkZk/9txipugWSmc057HdklMKPLOHWP0z5hLH0IbVIKXzdNISABm36jwQ== dependencies: core-js "^3.8.2" ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.4.14.tgz#d2053511971e06d70bba2accfbd1f6c0f2084e2a" - integrity sha512-hqdgE0zKVhcqG/8t/veJRgjsOT076LeKxoA+w2Ga4iU+reIGui/GvLsjvyFFTyOMHVeo2Ze4LW63oTYKF/I5iQ== +"@storybook/client-api@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.4.19.tgz#131597e160f112f51240a4e407191053e5ed972f" + integrity sha512-OCrT5Um3FDvZnimQKwWtwsaI+5agPwq2i8YiqlofrI/NPMKp0I7DEkCGwE5IRD1Q8BIKqHcMo5tTmfYi0AxyOg== dependencies: - "@storybook/addons" "6.4.14" - "@storybook/channel-postmessage" "6.4.14" - "@storybook/channels" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/channel-postmessage" "6.4.19" + "@storybook/channels" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/core-events" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/store" "6.4.14" + "@storybook/store" "6.4.19" "@types/qs" "^6.9.5" "@types/webpack-env" "^1.16.0" core-js "^3.8.2" @@ -1568,23 +1568,23 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-logger@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.4.14.tgz#a7aed982407e4146548f9ac4b3af5eba24cd045e" - integrity sha512-4VmFWZxhpeiG5fDhfqAyQbCfXZSBKS4fNKf35ABWiHStZRDndxml8K5WFtmOmMvVzjrGQx8HesenYMawK6xo/Q== +"@storybook/client-logger@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.4.19.tgz#b2011ad2fa446cce4a9afdb41974b2a576e9fad2" + integrity sha512-zmg/2wyc9W3uZrvxaW4BfHcr40J0v7AGslqYXk9H+ERLVwIvrR4NhxQFaS6uITjBENyRDxwzfU3Va634WcmdDQ== dependencies: core-js "^3.8.2" global "^4.4.0" -"@storybook/components@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/components/-/components-6.4.14.tgz#546b34fe3feb09e670b76ff71d889bf5f566f1e4" - integrity sha512-M7unerbOnvg+UN7qPxBCBWzK/boVdSSQxRiPAr1OL3M4OyEU8+TNPdQeAG0aF4zqtU0BrsDf4E85EznoMXUiFQ== +"@storybook/components@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/components/-/components-6.4.19.tgz#084ba21f26a3eeab82f45178de6899688eecb2fc" + integrity sha512-q/0V37YAJA7CNc+wSiiefeM9+3XVk8ixBNylY36QCGJgIeGQ5/79vPyUe6K4lLmsQwpmZsIq1s1Ad5+VbboeOA== dependencies: "@popperjs/core" "^2.6.0" - "@storybook/client-logger" "6.4.14" + "@storybook/client-logger" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/theming" "6.4.14" + "@storybook/theming" "6.4.19" "@types/color-convert" "^2.0.0" "@types/overlayscrollbars" "^1.12.0" "@types/react-syntax-highlighter" "11.0.5" @@ -1606,21 +1606,21 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/core-client@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.4.14.tgz#8f5dc6fe2295e479225bd396404a43679a15637e" - integrity sha512-e9pzKz52DVhmo8+sUEDvagwGKVqWZ6NQBIt3mBvd79/zXTPkFRnSVitOyYErqhgN1kuwocTg+2BigRr3H0qXaQ== +"@storybook/core-client@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.4.19.tgz#fc6902c4321ae9e7c2858126172bc0752a84321c" + integrity sha512-rQHRZjhArPleE7/S8ZUolgzwY+hC0smSKX/3PQxO2GcebDjnJj6+iSV3h+aSMHMmTdoCQvjYw9aBpT8scuRe+A== dependencies: - "@storybook/addons" "6.4.14" - "@storybook/channel-postmessage" "6.4.14" - "@storybook/channel-websocket" "6.4.14" - "@storybook/client-api" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/channel-postmessage" "6.4.19" + "@storybook/channel-websocket" "6.4.19" + "@storybook/client-api" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/core-events" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/preview-web" "6.4.14" - "@storybook/store" "6.4.14" - "@storybook/ui" "6.4.14" + "@storybook/preview-web" "6.4.19" + "@storybook/store" "6.4.19" + "@storybook/ui" "6.4.19" airbnb-js-shims "^2.2.1" ansi-to-html "^0.6.11" core-js "^3.8.2" @@ -1632,10 +1632,10 @@ unfetch "^4.2.0" util-deprecate "^1.0.2" -"@storybook/core-common@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/core-common/-/core-common-6.4.14.tgz#137b935855f0cc785ec55b386312747949e30e99" - integrity sha512-7NRmtcY2INmobsmUUX4afO78RHpyQMO8vboy6H8HRtfcw6fy4zaHoCb7gZZfvvn8gtBWNmwip8I9XK5BpRrh3Q== +"@storybook/core-common@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/core-common/-/core-common-6.4.19.tgz#18e6c6095ebd9a94b074529917c693084921d3ca" + integrity sha512-X1pJJkO48DFxl6iyEemIKqRkJ7j9/cBh3BRBUr+xZHXBvnD0GKDXIocwh0PjSxSC6XSu3UCQnqtKi3PbjRl8Dg== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-proposal-class-properties" "^7.12.1" @@ -1658,7 +1658,7 @@ "@babel/preset-react" "^7.12.10" "@babel/preset-typescript" "^7.12.7" "@babel/register" "^7.12.1" - "@storybook/node-logger" "6.4.14" + "@storybook/node-logger" "6.4.19" "@storybook/semver" "^7.3.2" "@types/node" "^14.0.10" "@types/pretty-hrtime" "^1.0.0" @@ -1687,29 +1687,29 @@ util-deprecate "^1.0.2" webpack "4" -"@storybook/core-events@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.4.14.tgz#37293c0fce703f2643cec6f24fc6ef7c40e30ded" - integrity sha512-9QFltg2mxTDjMBfmVtFHtrAEPY/i0oVp2kVdTWo6g05cPffYKAjNUnUVjUl7yiqcQmdEcdqUUQ0ut3xgmcYi/A== +"@storybook/core-events@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.4.19.tgz#d2a03156783a3cb9bd9f7ba81a06a798a5c296ae" + integrity sha512-KICzUw6XVQUJzFSCXfvhfHAuyhn4Q5J4IZEfuZkcGJS4ODkrO6tmpdYE5Cfr+so95Nfp0ErWiLUuodBsW9/rtA== dependencies: core-js "^3.8.2" -"@storybook/core-server@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.4.14.tgz#0bef36e1203eb56e1c9bbf7f02122500c8f7d534" - integrity sha512-SzO8SaLTZ36Q4PNhJD4XJjlnonbR2Os0gzTknDBbwyIRPUtFUdk6isSG14RM5yYWPM0QQIs9og5ztSPX58YZlw== +"@storybook/core-server@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.4.19.tgz#0d1b4b2094749b8bce03e3d01422e14e5fef8e66" + integrity sha512-bKsUB9f7hl5ya2JXxpIrErmbDQjoH39FVbzYZWjMo4t/b7+Xyi6vYadwyWcqlpUQmis09ZaSMv8L/Tw0TuwLAA== dependencies: "@discoveryjs/json-ext" "^0.5.3" - "@storybook/builder-webpack4" "6.4.14" - "@storybook/core-client" "6.4.14" - "@storybook/core-common" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/builder-webpack4" "6.4.19" + "@storybook/core-client" "6.4.19" + "@storybook/core-common" "6.4.19" + "@storybook/core-events" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/csf-tools" "6.4.14" - "@storybook/manager-webpack4" "6.4.14" - "@storybook/node-logger" "6.4.14" + "@storybook/csf-tools" "6.4.19" + "@storybook/manager-webpack4" "6.4.19" + "@storybook/node-logger" "6.4.19" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.14" + "@storybook/store" "6.4.19" "@types/node" "^14.0.10" "@types/node-fetch" "^2.5.7" "@types/pretty-hrtime" "^1.0.0" @@ -1742,18 +1742,18 @@ webpack "4" ws "^8.2.3" -"@storybook/core@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/core/-/core-6.4.14.tgz#c20a1432f22603eb2d3523389ff1311fffbba24f" - integrity sha512-41WNDXKMZuCKnvbLBBYCd1+ip4uJ4AGeCOhmp/KZK7TgkitJ0JrvyRgnbpXR8bAMiOv2Hh9t9Vmi5D3QZ8COlg== +"@storybook/core@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/core/-/core-6.4.19.tgz#58dd055bcc0ef335e0e0d3f6eca74b4d4d49eba1" + integrity sha512-55LOQ/h/kf1jMhjN85t/pIEdIwWEG9yV7bdwv3niVvmoypCxyyjn9/QNK0RKYAeDSUtdm6FVoJ6k5CpxWz2d8w== dependencies: - "@storybook/core-client" "6.4.14" - "@storybook/core-server" "6.4.14" + "@storybook/core-client" "6.4.19" + "@storybook/core-server" "6.4.19" -"@storybook/csf-tools@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-6.4.14.tgz#c5112e17f07dae4c7b922aefd45dccbbc9e49803" - integrity sha512-mRFsIhzFA2JBeUqdvl6+WM6HmHXaWGLbCgalzGqX65i1pSvhmC3jHh0OTTypMj9XneWH6/cHQh7LvivYbjJ8Cg== +"@storybook/csf-tools@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-6.4.19.tgz#28bdea11da17501a8bc4e761b821d7721880eaf6" + integrity sha512-gf/zRhGoAVsFwSyV2tc+jeJfZQkxF6QsaZgbUSe24/IUvGFCT/PS/jZq1qy7dECAwrTOfykgu8juyBtj6WhWyw== dependencies: "@babel/core" "^7.12.10" "@babel/generator" "^7.12.11" @@ -1780,20 +1780,20 @@ dependencies: lodash "^4.17.15" -"@storybook/manager-webpack4@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.4.14.tgz#b8ca3a11d0fb18ef6ca3e58e1c36b2eb8226ccbf" - integrity sha512-j565G7vZLBXK60J1hiZhbeZ6K48y8CMMZCcIihqsFv/4jj0kI3Ba4IhCrOkHiqiRM89mRu5/Ga3DnHTBvIYIEA== +"@storybook/manager-webpack4@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.4.19.tgz#999577afb9b9a57fc478f7c5e5d95d785ea69da3" + integrity sha512-R8ugZjTYqXvlc6gDOcw909L65sIleOmIJLZR+N6/H85MivGXHu39jOwONqB7tVACufRty4FNecn8tEiQL2SAKA== dependencies: "@babel/core" "^7.12.10" "@babel/plugin-transform-template-literals" "^7.12.1" "@babel/preset-react" "^7.12.10" - "@storybook/addons" "6.4.14" - "@storybook/core-client" "6.4.14" - "@storybook/core-common" "6.4.14" - "@storybook/node-logger" "6.4.14" - "@storybook/theming" "6.4.14" - "@storybook/ui" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/core-client" "6.4.19" + "@storybook/core-common" "6.4.19" + "@storybook/node-logger" "6.4.19" + "@storybook/theming" "6.4.19" + "@storybook/ui" "6.4.19" "@types/node" "^14.0.10" "@types/webpack" "^4.41.26" babel-loader "^8.0.0" @@ -1822,10 +1822,10 @@ webpack-dev-middleware "^3.7.3" webpack-virtual-modules "^0.2.2" -"@storybook/node-logger@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.4.14.tgz#2e96f4e3e06c78c3d065e59818515209122d9ae4" - integrity sha512-mowC0adx4hLtCqGMQKRfNmiRYAL2PYdk3ojc91qzIKNrjSYnE4U8d9qlw5WLx1PKEnZVji3+QiYfNHpA/8PoKw== +"@storybook/node-logger@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.4.19.tgz#554f9efad4e95ce6fa63222d026f43258293c896" + integrity sha512-hO2Aar3PgPnPtNq2fVgiuGlqo3EEVR6TKVBXMq7foL3tN2k4BQFKLDHbm5qZQQntyYKurKsRUGKPJFPuI1ov/w== dependencies: "@types/npmlog" "^4.1.2" chalk "^4.1.0" @@ -1833,17 +1833,17 @@ npmlog "^5.0.1" pretty-hrtime "^1.0.3" -"@storybook/preview-web@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/preview-web/-/preview-web-6.4.14.tgz#4d7035d5aa0e8c41c9a2ff21c2a3b3cbae9f3688" - integrity sha512-3E++OYz+OCyJBIchkNCJRtxEU7XNDBdIvKRTCx48X+Uv5qoLeCpXiXOSK/42LlraWZkfBs56yHv9VSqJoQ8VwA== +"@storybook/preview-web@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/preview-web/-/preview-web-6.4.19.tgz#bdfab7b2f760caf72140229dd64fd57617ad000b" + integrity sha512-jqltoBv5j7lvnxEfV9w8dLX9ASWGuvgz97yg8Yo5FqkftEwrHJenyvMGcTgDJKJPorF+wiz/9aIqnmd3LCAcZQ== dependencies: - "@storybook/addons" "6.4.14" - "@storybook/channel-postmessage" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/channel-postmessage" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/core-events" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/store" "6.4.14" + "@storybook/store" "6.4.19" ansi-to-html "^0.6.11" core-js "^3.8.2" global "^4.4.0" @@ -1868,22 +1868,22 @@ react-docgen-typescript "^2.0.0" tslib "^2.0.0" -"@storybook/react@^6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/react/-/react-6.4.14.tgz#7be241ecfa412312681bb54c82327764d70ffb70" - integrity sha512-wlPjE5Xcarc5NTgnHchvGE56EVYioAyRZoYvb/YyiCX1+A8sQkwS2qTTH8e/pdG539A4NMrciMosvjvEPZcEvg== +"@storybook/react@^6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/react/-/react-6.4.19.tgz#1707b785b5a65c867e291ede12113e7fd55f8998" + integrity sha512-5b3i8jkVrjQGmcxxxXwCduHPIh+cluWkfeweKeQOe+lW4BR8fuUICo3AMLrYPAtB/UcaJyYkIYmTvF2mkfepFA== dependencies: "@babel/preset-flow" "^7.12.1" "@babel/preset-react" "^7.12.10" "@pmmmwh/react-refresh-webpack-plugin" "^0.5.1" - "@storybook/addons" "6.4.14" - "@storybook/core" "6.4.14" - "@storybook/core-common" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/core" "6.4.19" + "@storybook/core-common" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" - "@storybook/node-logger" "6.4.14" + "@storybook/node-logger" "6.4.19" "@storybook/react-docgen-typescript-plugin" "1.0.2-canary.253f8c1.0" "@storybook/semver" "^7.3.2" - "@storybook/store" "6.4.14" + "@storybook/store" "6.4.19" "@types/webpack-env" "^1.16.0" babel-plugin-add-react-displayname "^0.0.5" babel-plugin-named-asset-import "^0.3.1" @@ -1898,12 +1898,12 @@ ts-dedent "^2.0.0" webpack "4" -"@storybook/router@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/router/-/router-6.4.14.tgz#46fd46eadafc0d6b647be13702704c5fcf8f11e3" - integrity sha512-5+tePyINtwPYm4izgOBZ2sX2ViWtfmmO2vwOAPlWWEGzsRosVQsGMdZv1R8rk4Jl/TotMjlTmd8I1/BufEeIeQ== +"@storybook/router@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.4.19.tgz#e653224dd9a521836bbd2610f604f609a2c77af2" + integrity sha512-KWWwIzuyeEIWVezkCihwY2A76Il9tUNg0I410g9qT7NrEsKyqXGRYOijWub7c1GGyNjLqz0jtrrehtixMcJkuA== dependencies: - "@storybook/client-logger" "6.4.14" + "@storybook/client-logger" "6.4.19" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -1923,13 +1923,13 @@ core-js "^3.6.5" find-up "^4.1.0" -"@storybook/source-loader@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.4.14.tgz#d1e6c2df0918e4867b3e4b5ce748685938f77d87" - integrity sha512-3hqVTK5+rQFK7Jf6/jYO/24daYIMn9L1vCAo9xSFgy999OMw7967ZmVMGMgVkOh7GQSZmzt3kMonv4bDmIGJMw== +"@storybook/source-loader@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-6.4.19.tgz#24d134750bc41a13255b2b4a545f2d82613f004f" + integrity sha512-XqTsqddRglvfW7mhyjwoqd/B8L6samcBehhO0OEbsFp6FPWa9eXuObCxtRYIcjcSIe+ksbW3D/54ppEs1L/g1Q== dependencies: - "@storybook/addons" "6.4.14" - "@storybook/client-logger" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/client-logger" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" estraverse "^5.2.0" @@ -1939,14 +1939,14 @@ prettier ">=2.2.1 <=2.3.0" regenerator-runtime "^0.13.7" -"@storybook/store@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/store/-/store-6.4.14.tgz#2ec5601e1c40a27f164b570d4c2b84c57d3a4115" - integrity sha512-D9KoJuNvwb9mEQD60GTPYSbQuXWZQHE8RBxCq7d7Qu46mrhlsNTOwt09lIgmuM3jAVto3FxnXY4U81RwJza7tg== +"@storybook/store@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/store/-/store-6.4.19.tgz#bf4031499f4d49909d7b691c03cc5ef1ec00ad74" + integrity sha512-N9/ZjemRHGfT3InPIbqQqc6snkcfnf3Qh9oOr0smbfaVGJol//KOX65kzzobtzFcid0WxtTDZ3HmgFVH+GvuhQ== dependencies: - "@storybook/addons" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/core-events" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/core-events" "6.4.19" "@storybook/csf" "0.0.2--canary.87bc651.0" core-js "^3.8.2" fast-deep-equal "^3.1.3" @@ -1960,15 +1960,15 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/theming@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.4.14.tgz#f034914eb1853a80f588c7c141d47af0595f6d1f" - integrity sha512-kqmXNnIoOSAS4cgr9PitMgVrOps725O99eTsJNxB6J1Ide0CsA5v2tV6AmQn/scnpCQNr8uSjZerNlEcl/ensg== +"@storybook/theming@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.4.19.tgz#0a6834d91e0b0eadbb10282e7fb2947e2bbf9e9e" + integrity sha512-V4pWmTvAxmbHR6B3jA4hPkaxZPyExHvCToy7b76DpUTpuHihijNDMAn85KhOQYIeL9q14zP/aiz899tOHsOidg== dependencies: "@emotion/core" "^10.1.1" "@emotion/is-prop-valid" "^0.8.6" "@emotion/styled" "^10.0.27" - "@storybook/client-logger" "6.4.14" + "@storybook/client-logger" "6.4.19" core-js "^3.8.2" deep-object-diff "^1.1.0" emotion-theming "^10.0.27" @@ -1978,21 +1978,21 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" -"@storybook/ui@6.4.14": - version "6.4.14" - resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.4.14.tgz#59f08ac8d8eb782fa13fc9a8dd715222c96bf234" - integrity sha512-nZsd8GXzYwmmTjZUB7pJMh+Q1fST0d2lFkhDHakxLaPLwumibw9NHJ7bRWYHFlAVYpD0c2+POP3FpOW5Bjby1A== +"@storybook/ui@6.4.19": + version "6.4.19" + resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.4.19.tgz#1fb9f6cd875ee4937cf9d81ca45d5156800176d1" + integrity sha512-gFwdn5LA2U6oQ4bfUFLyHZnNasGQ01YVdwjbi+l6yjmnckBNtZfJoVTZ1rzGUbxSE9rK48InJRU+latTsr7xAg== dependencies: "@emotion/core" "^10.1.1" - "@storybook/addons" "6.4.14" - "@storybook/api" "6.4.14" - "@storybook/channels" "6.4.14" - "@storybook/client-logger" "6.4.14" - "@storybook/components" "6.4.14" - "@storybook/core-events" "6.4.14" - "@storybook/router" "6.4.14" + "@storybook/addons" "6.4.19" + "@storybook/api" "6.4.19" + "@storybook/channels" "6.4.19" + "@storybook/client-logger" "6.4.19" + "@storybook/components" "6.4.19" + "@storybook/core-events" "6.4.19" + "@storybook/router" "6.4.19" "@storybook/semver" "^7.3.2" - "@storybook/theming" "6.4.14" + "@storybook/theming" "6.4.19" copy-to-clipboard "^3.3.1" core-js "^3.8.2" core-js-pure "^3.8.2" diff --git a/yarn.lock b/yarn.lock index ee92b9f917..90a6ef9c24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -311,7 +311,7 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3": version "7.16.7" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== @@ -1392,17 +1392,17 @@ resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-5.0.2.tgz#b23080fa35520e993a8a37a0f5bca26aa4650a48" integrity sha512-NBEJlHWrhQucLhZGHtSxM2loSaNUMajC7KOYJLyfcdW/6goVoff2HoYI3bz8YCDN0wKGbxtUL0gx2dvHpvnWlw== -"@changesets/apply-release-plan@^5.0.1": - version "5.0.1" - resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-5.0.1.tgz#ed3e30550f787ef1b72f0a51e29a54d244123109" - integrity sha512-ltYLM/PPoL1Un9hnNCbUac25FWonJvIZ/9C3O4UyZ/k4rir9FGvH6KLtMOiPEAJWnXmaHeRDr06MzohuXOnmvw== +"@changesets/apply-release-plan@^5.0.4": + version "5.0.4" + resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-5.0.4.tgz#fefffecf73b59dbee7ae905b3c6a2e64c489f0cb" + integrity sha512-czayDIrgC8qBnqwClvh9nxjCMem+XZG7xtfdYwq3dxpzA30qGppcI0i088VYug5RCFR+l1N+HUvkujSZuBK65w== dependencies: "@babel/runtime" "^7.10.4" - "@changesets/config" "^1.6.1" + "@changesets/config" "^1.6.4" "@changesets/get-version-range-type" "^0.3.2" - "@changesets/git" "^1.1.2" - "@changesets/types" "^4.0.1" - "@manypkg/get-packages" "^1.0.1" + "@changesets/git" "^1.3.0" + "@changesets/types" "^4.0.2" + "@manypkg/get-packages" "^1.1.3" detect-indent "^6.0.0" fs-extra "^7.0.1" lodash.startcase "^4.4.0" @@ -1411,45 +1411,45 @@ resolve-from "^5.0.0" semver "^5.4.1" -"@changesets/assemble-release-plan@^5.0.1": - version "5.0.1" - resolved "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-5.0.1.tgz#80e9b750705677eb2d6356c581ed9c2e97fd68e7" - integrity sha512-KQqafvScTFQ/4Q2LpLmDYhU47LWvIGcgVS8tzKU8fBvRdKuLGQXe42VYbwVM0cHIkFd/b6YFn+H2QMdKC2MjIQ== +"@changesets/assemble-release-plan@^5.0.5": + version "5.0.5" + resolved "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-5.0.5.tgz#60e4adc9bf458dd6ec38dee78d8f1bbeddfe3936" + integrity sha512-ejCVSM4I1jgaNi30we3/qltj2NQtS68w7C3H8Gvb6ZOvbIpAW/Tr0uMmPgRj4Vzkez5+fx0If02AvOdssz1btA== dependencies: "@babel/runtime" "^7.10.4" "@changesets/errors" "^0.1.4" - "@changesets/get-dependents-graph" "^1.2.2" - "@changesets/types" "^4.0.1" - "@manypkg/get-packages" "^1.0.1" + "@changesets/get-dependents-graph" "^1.3.0" + "@changesets/types" "^4.0.2" + "@manypkg/get-packages" "^1.1.3" semver "^5.4.1" "@changesets/cli@^2.14.0": - version "2.17.0" - resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.17.0.tgz#cc7ff4f64d029ddd6d87020a012c8cf8c7adde58" - integrity sha512-UyraYwYst1lTjef+8i80XQ6SqsLaGwi4Sgn9YuDf2xdHA9m+5qQXshHvHVjaTdPTA09rqMBk9yeO7vmAqF4+vQ== + version "2.20.0" + resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.20.0.tgz#f24dbd38bd4bc47bf083e59b6bb9cbf531465808" + integrity sha512-IUYSgZKtS+wXPD5hxfnCfZ1JWCbBI0CRrhxpkgVKcXDwpxiRU8stCwuSuVj14kiYlThuH2zL0/ZuGvhF4r28Gg== dependencies: "@babel/runtime" "^7.10.4" - "@changesets/apply-release-plan" "^5.0.1" - "@changesets/assemble-release-plan" "^5.0.1" - "@changesets/config" "^1.6.1" + "@changesets/apply-release-plan" "^5.0.4" + "@changesets/assemble-release-plan" "^5.0.5" + "@changesets/config" "^1.6.4" "@changesets/errors" "^0.1.4" - "@changesets/get-dependents-graph" "^1.2.2" - "@changesets/get-release-plan" "^3.0.1" - "@changesets/git" "^1.1.2" + "@changesets/get-dependents-graph" "^1.3.0" + "@changesets/get-release-plan" "^3.0.5" + "@changesets/git" "^1.3.0" "@changesets/logger" "^0.0.5" - "@changesets/pre" "^1.0.7" - "@changesets/read" "^0.5.0" - "@changesets/types" "^4.0.1" - "@changesets/write" "^0.1.5" - "@manypkg/get-packages" "^1.0.1" + "@changesets/pre" "^1.0.9" + "@changesets/read" "^0.5.3" + "@changesets/types" "^4.0.2" + "@changesets/write" "^0.1.6" + "@manypkg/get-packages" "^1.1.3" + "@types/is-ci" "^3.0.0" "@types/semver" "^6.0.0" - boxen "^1.3.0" chalk "^2.1.0" enquirer "^2.3.0" external-editor "^3.1.0" fs-extra "^7.0.1" human-id "^1.0.2" - is-ci "^2.0.0" + is-ci "^3.0.1" meow "^6.0.0" outdent "^0.5.0" p-limit "^2.2.0" @@ -1459,16 +1459,16 @@ term-size "^2.1.0" tty-table "^2.8.10" -"@changesets/config@^1.6.1": - version "1.6.1" - resolved "https://registry.npmjs.org/@changesets/config/-/config-1.6.1.tgz#e9b1636fd56a74411c493c924e6ffa07d7d26091" - integrity sha512-aQTo6ODvhsvnSFszMP1YbJyAi1DtE1Pes9rL+G+KYJiAOA6k5RzbiKOarjo+ZkKXpX0G3CBAbOO8jXOX4xG7cQ== +"@changesets/config@^1.6.4": + version "1.6.4" + resolved "https://registry.npmjs.org/@changesets/config/-/config-1.6.4.tgz#c904c84f067b49c09b378618b751089dd29ed0b7" + integrity sha512-WWa8eR8GzS/p2atLc/+5UEDn7fsRCZ+/sShLkB/3efVbTkSTB1PwoKwQRXLYXM1DY289T7UnJT4HLZA3Gcreww== dependencies: "@changesets/errors" "^0.1.4" - "@changesets/get-dependents-graph" "^1.2.2" + "@changesets/get-dependents-graph" "^1.3.0" "@changesets/logger" "^0.0.5" - "@changesets/types" "^4.0.1" - "@manypkg/get-packages" "^1.0.1" + "@changesets/types" "^4.0.2" + "@manypkg/get-packages" "^1.1.3" fs-extra "^7.0.1" micromatch "^4.0.2" @@ -1479,44 +1479,44 @@ dependencies: extendable-error "^0.1.5" -"@changesets/get-dependents-graph@^1.2.2": - version "1.2.2" - resolved "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.2.2.tgz#7a2238f3d1a023de83d37b727a0da15826e88d73" - integrity sha512-3zJRw6TcexmOrmIZNOXpIRsZtqtrdmlzbqp4+V0VgnBvTxz16rqCS9VBsBqFYeJDWFj3soOlHUMeTwLghr18DA== +"@changesets/get-dependents-graph@^1.3.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.3.0.tgz#892bbb63406911baac1f77a31303620492d466ec" + integrity sha512-4VHQWEluWySPgDdkL94YNxrEjDb9nwNFw515sWDmVrlfpQN5qaP1hdaotrp4mJm4ky85t4cTlrWSP+CTY7IDbw== dependencies: - "@changesets/types" "^4.0.1" - "@manypkg/get-packages" "^1.0.1" + "@changesets/types" "^4.0.2" + "@manypkg/get-packages" "^1.1.3" chalk "^2.1.0" fs-extra "^7.0.1" semver "^5.4.1" -"@changesets/get-release-plan@^3.0.1": - version "3.0.1" - resolved "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-3.0.1.tgz#c98a34321eac9e4187098893ff8dadb6f90ad89c" - integrity sha512-HTZeEPvLlcWMWKxLrzQNLQWKDDN1lUKvaOV+hl/yBhgtyJECljJJzd3IRaKqCSWMrYKNaaEcmunTtZ4oaeoK9w== +"@changesets/get-release-plan@^3.0.5": + version "3.0.5" + resolved "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-3.0.5.tgz#c05cff031bba737c8dd236afe97e77b007ee44a3" + integrity sha512-67td3LA1RTJpY5Q+wJaTTRtAjZ2suAhDfj3VRjFv0gCgUPXs8rNx17n9UPbegPTQjeTS1r7hVRVifycmT0fQtA== dependencies: "@babel/runtime" "^7.10.4" - "@changesets/assemble-release-plan" "^5.0.1" - "@changesets/config" "^1.6.1" - "@changesets/pre" "^1.0.7" - "@changesets/read" "^0.5.0" - "@changesets/types" "^4.0.1" - "@manypkg/get-packages" "^1.0.1" + "@changesets/assemble-release-plan" "^5.0.5" + "@changesets/config" "^1.6.4" + "@changesets/pre" "^1.0.9" + "@changesets/read" "^0.5.3" + "@changesets/types" "^4.0.2" + "@manypkg/get-packages" "^1.1.3" "@changesets/get-version-range-type@^0.3.2": version "0.3.2" resolved "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.3.2.tgz#8131a99035edd11aa7a44c341cbb05e668618c67" integrity sha512-SVqwYs5pULYjYT4op21F2pVbcrca4qA/bAA3FmFXKMN7Y+HcO8sbZUTx3TAy2VXulP2FACd1aC7f2nTuqSPbqg== -"@changesets/git@^1.1.2": - version "1.1.2" - resolved "https://registry.npmjs.org/@changesets/git/-/git-1.1.2.tgz#248d4418bcb2d4f198852409cfcbd06a1fcb0424" - integrity sha512-dfza8elsIwcYVa4fFzLaPs4+AkoCFiW3sfzkkC7WR+rG9j+zZh7CelzVpnoiAbEI2QOzeCbZKMoLSvBPgHhB1g== +"@changesets/git@^1.3.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@changesets/git/-/git-1.3.0.tgz#7f62e1983552efecb738054db5178eef77f41241" + integrity sha512-Ydj4lWX33d2PCDaTXOMSbyTjgk1go1V6EyXjKTmOV7nB/qvgKdDZLSt+AexKWKp3Ac2FTrtVnl9G5gMNVYNmuQ== dependencies: "@babel/runtime" "^7.10.4" "@changesets/errors" "^0.1.4" - "@changesets/types" "^4.0.1" - "@manypkg/get-packages" "^1.0.1" + "@changesets/types" "^4.0.2" + "@manypkg/get-packages" "^1.1.3" is-subdir "^1.1.1" spawndamnit "^2.0.0" @@ -1527,51 +1527,51 @@ dependencies: chalk "^2.1.0" -"@changesets/parse@^0.3.9": - version "0.3.9" - resolved "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.9.tgz#c518792b05f15ab418d58dc1cea81601556e845e" - integrity sha512-XoTEkMpvRRVxSlhvOaK4YSFM+RZhYFTksxRh7ieNkb6pMxkpq8MOYSi/07BuqkODn4dJEMOoSy3RzL99P6FyqA== +"@changesets/parse@^0.3.11": + version "0.3.11" + resolved "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.11.tgz#b53485f36152e6e9a097b56fc46cafb6705ce2b5" + integrity sha512-w5/X8KijcCrvv5lHimXIBR9o35c78niiBoesBjBUlWeifwPz0DHc/lzVYJKRkA5w0BGqft6T/9hKI68GaYj5wA== dependencies: - "@changesets/types" "^4.0.1" + "@changesets/types" "^4.0.2" js-yaml "^3.13.1" -"@changesets/pre@^1.0.7": - version "1.0.7" - resolved "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.7.tgz#caf6430c90b8ac6d58c9cd90a19558ab06b19b88" - integrity sha512-oUU6EL4z0AIyCv/EscQFxxJsQfc9/AcSpqAGbdZrLXwshUWTXsJHMWlE3/+iSIyQ+I+/xtxbBxnqDUpUU3TOOg== +"@changesets/pre@^1.0.9": + version "1.0.9" + resolved "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.9.tgz#f1a0efea42733c25ef4a782377b2ac61023bd1b7" + integrity sha512-F3+qMun89KlynecBD15fEpwGT/KxbYb3WGeut6w1xhZb0u7V/jdcPy9b+kJ2xmBqFZLn1WteWIP96IjxS57H7A== dependencies: "@babel/runtime" "^7.10.4" "@changesets/errors" "^0.1.4" - "@changesets/types" "^4.0.1" - "@manypkg/get-packages" "^1.0.1" + "@changesets/types" "^4.0.2" + "@manypkg/get-packages" "^1.1.3" fs-extra "^7.0.1" -"@changesets/read@^0.5.0": - version "0.5.0" - resolved "https://registry.npmjs.org/@changesets/read/-/read-0.5.0.tgz#52f7a10f6baebf97172e62035ee8345652c5a1c0" - integrity sha512-A2OJ+vgfvbUaLx2yKyHH+tapa+DUd2NtpFpVuxjUqv0zirjqju20z1bziqaqpIQSf/rXPuoc09vp5w4VakraHg== +"@changesets/read@^0.5.3": + version "0.5.3" + resolved "https://registry.npmjs.org/@changesets/read/-/read-0.5.3.tgz#5afb14ee0e806751c255e94fe8742843a9b12487" + integrity sha512-zoj5NjNR4AhiGXz6aHTxsBLojChHgDOSbz6VfAVxMKX7tF7UhyNYptG2VEbSjxeamNKABx6k1pkM2IyVVlOcbQ== dependencies: "@babel/runtime" "^7.10.4" - "@changesets/git" "^1.1.2" + "@changesets/git" "^1.3.0" "@changesets/logger" "^0.0.5" - "@changesets/parse" "^0.3.9" - "@changesets/types" "^4.0.1" + "@changesets/parse" "^0.3.11" + "@changesets/types" "^4.0.2" chalk "^2.1.0" fs-extra "^7.0.1" p-filter "^2.1.0" -"@changesets/types@^4.0.1": - version "4.0.1" - resolved "https://registry.npmjs.org/@changesets/types/-/types-4.0.1.tgz#85cf3cc32baff0691112d9d15fc21fbe022c9f0a" - integrity sha512-zVfv752D8K2tjyFmxU/vnntQ+dPu+9NupOSguA/2Zuym4tVxRh0ylArgKZ1bOAi2eXfGlZMxJU/kj7uCSI15RQ== +"@changesets/types@^4.0.1", "@changesets/types@^4.0.2": + version "4.0.2" + resolved "https://registry.npmjs.org/@changesets/types/-/types-4.0.2.tgz#d20e1e45bdc96a97cc509c655e708b53a9292465" + integrity sha512-OeDaB7D+WVy/ErymPzFm58IeGvz4DOl+oedyZETfnkfMezF/Uhrm1Ub6MHrO5LcAaQTW+ptDmr0fmaVyoTxgHw== -"@changesets/write@^0.1.5": - version "0.1.5" - resolved "https://registry.npmjs.org/@changesets/write/-/write-0.1.5.tgz#97574d95c8e48c3bbb1173802672f9a64d1b7fef" - integrity sha512-AYVSCH7on/Cyzo/8lVfqlsXmyKl3JhbNu9yHApdLPhHAzv5wqoHiZlMDkmd+AA67SRqzK2lDs4BcIojK+uWeIA== +"@changesets/write@^0.1.6": + version "0.1.6" + resolved "https://registry.npmjs.org/@changesets/write/-/write-0.1.6.tgz#798d882bada93f391902a854a17965f3b80a5201" + integrity sha512-JWE2gJs9eHhorxqembkf43fllKlCz+sp1TJKSheaWfhWILMHPdfa/xQG4+sMZkISo1qZ+IlJyiBLha6iGGjXyA== dependencies: "@babel/runtime" "^7.10.4" - "@changesets/types" "^4.0.1" + "@changesets/types" "^4.0.2" fs-extra "^7.0.1" human-id "^1.0.2" prettier "^1.19.1" @@ -1722,11 +1722,6 @@ resolved "https://registry.npmjs.org/@date-io/core/-/core-2.10.7.tgz#0fe1fa0ef02c827919e23c2802a4b25589ac522d" integrity sha512-EG/1qDiQvd12RoNJ6H+sZcHVswC/3uMx/ySvfaJ24vB30rLjkgHggEXbgMbfgki7wMuiQ/zXI8QlmF1k3kWRGQ== -"@date-io/core@^2.13.1": - version "2.13.1" - resolved "https://registry.npmjs.org/@date-io/core/-/core-2.13.1.tgz#f041765aff5c55fbc7e37fdd75fc1792733426d6" - integrity sha512-pVI9nfkf2qClb2Cxdq0Q4zJhdawMG4ybWZUVGifT78FDwzRMX2SwXBb55s5NRJk0HcIicDuxktmCtemZqMH1Zg== - "@date-io/date-fns@^1.3.13": version "1.3.13" resolved "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz#7798844041640ab393f7e21a7769a65d672f4735" @@ -1734,21 +1729,21 @@ dependencies: "@date-io/core" "^1.3.13" -"@date-io/luxon@2.x": - version "2.13.1" - resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.13.1.tgz#3701b3cabfffda5102af302979aa6e58acfda91a" - integrity sha512-yG+uM7lXfwLyKKEwjvP8oZ7qblpmfl9gxQYae55ifbwiTs0CoCTkYkxEaQHGkYtTqGTzLqcb0O9Pzx6vgWg+yg== +"@date-io/luxon@1.x", "@date-io/luxon@^1.3.13": + version "1.3.13" + resolved "https://registry.npmjs.org/@date-io/luxon/-/luxon-1.3.13.tgz#68f0134bb38ef486b2ed6df01981f814c633e28a" + integrity sha512-9wUrJCNSMZJeYAiH+dbb45oGpnHeFP7TOH/Lt26If47gjFCkjvyINzWx+K5AGsnlP0Qosxc7hkF1yLi6ecutxw== dependencies: - "@date-io/core" "^2.13.1" + "@date-io/core" "^1.3.13" -"@elastic/elasticsearch-mock@^0.3.0": - version "0.3.0" - resolved "https://registry.npmjs.org/@elastic/elasticsearch-mock/-/elasticsearch-mock-0.3.0.tgz#6b1d8448aad3ca20f760fa01c0206b733c9c1e54" - integrity sha512-hZYRjPgRE1M0wCqdsgaDtwxrgQEXDZya1gQ3gnpc8pB8mHUfPoO+9ye7GbDPUkWbuGGGZ4/p6OKmAbt/ME+CDQ== +"@elastic/elasticsearch-mock@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@elastic/elasticsearch-mock/-/elasticsearch-mock-1.0.0.tgz#1fbaf6bf220cd74a34494ff3a8673551d9dfc66f" + integrity sha512-MajkXUZ1xMYJilZc4f6KxZ9fbOp3VkUgjshZOm3Z4dbzEpRz7FinOfo6/Om7qdFyzhCJ9GgPQz4b0zp+3QMeqQ== dependencies: - fast-deep-equal "^3.1.1" - find-my-way "^2.2.2" - into-stream "^5.1.1" + fast-deep-equal "^3.1.3" + find-my-way "^4.3.3" + into-stream "^6.0.0" "@elastic/elasticsearch@7.13.0": version "7.13.0" @@ -2539,11 +2534,21 @@ resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@hutson/parse-repository-url@^3.0.0": + version "3.0.2" + resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340" + integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== + "@iarna/toml@^2.2.5": version "2.2.5" resolved "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== +"@isaacs/string-locale-compare@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz#291c227e93fd407a96ecd59879a35809120e432b" + integrity sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b" @@ -3477,17 +3482,6 @@ find-up "^4.1.0" fs-extra "^8.1.0" -"@manypkg/get-packages@^1.0.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.1.tgz#7c7e72d0061ab2e61d2ce4da58ce91290a60ac8d" - integrity sha512-J6VClfQSVgR6958eIDTGjfdCrELy1eT+SHeoSMomnvRQVktZMnEA5edIr5ovRFNw5y+Bk/jyoevPzGYod96mhw== - dependencies: - "@babel/runtime" "^7.5.5" - "@manypkg/find-root" "^1.1.0" - fs-extra "^8.1.0" - globby "^11.0.0" - read-yaml-file "^1.1.0" - "@manypkg/get-packages@^1.1.3": version "1.1.3" resolved "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-1.1.3.tgz#e184db9bba792fa4693de4658cfb1463ac2c9c47" @@ -3532,7 +3526,7 @@ react-beautiful-dnd "^13.0.0" react-double-scrollbar "0.0.15" -"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2": +"@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.1", "@material-ui/core@^4.12.2", "@material-ui/core@^4.9.13": version "4.12.3" resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== @@ -3753,20 +3747,21 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" -"@npmcli/arborist@^2.2.2": - version "2.8.3" - resolved "https://registry.npmjs.org/@npmcli/arborist/-/arborist-2.8.3.tgz#5569e7d2038f6893abc81f9c879f497b506e6980" - integrity sha512-miFcxbZjmQqeFTeRSLLh+lc/gxIKDO5L4PVCp+dp+kmcwJmYsEJmF7YvHR2yi3jF+fxgvLf3CCFzboPIXAuabg== +"@npmcli/arborist@^4.0.4": + version "4.3.1" + resolved "https://registry.npmjs.org/@npmcli/arborist/-/arborist-4.3.1.tgz#a08cddce3339882f688c1dea1651f6971e781c44" + integrity sha512-yMRgZVDpwWjplorzt9SFSaakWx6QIK248Nw4ZFgkrAy/GvJaFRaSZzE6nD7JBK5r8g/+PTxFq5Wj/sfciE7x+A== dependencies: + "@isaacs/string-locale-compare" "^1.1.0" "@npmcli/installed-package-contents" "^1.0.7" - "@npmcli/map-workspaces" "^1.0.2" - "@npmcli/metavuln-calculator" "^1.1.0" + "@npmcli/map-workspaces" "^2.0.0" + "@npmcli/metavuln-calculator" "^2.0.0" "@npmcli/move-file" "^1.1.0" "@npmcli/name-from-folder" "^1.0.1" - "@npmcli/node-gyp" "^1.0.1" + "@npmcli/node-gyp" "^1.0.3" "@npmcli/package-json" "^1.0.1" - "@npmcli/run-script" "^1.8.2" - bin-links "^2.2.1" + "@npmcli/run-script" "^2.0.0" + bin-links "^3.0.0" cacache "^15.0.3" common-ancestor-path "^1.0.1" json-parse-even-better-errors "^2.3.1" @@ -3776,9 +3771,9 @@ npm-install-checks "^4.0.0" npm-package-arg "^8.1.5" npm-pick-manifest "^6.1.0" - npm-registry-fetch "^11.0.0" - pacote "^11.3.5" - parse-conflict-json "^1.1.1" + npm-registry-fetch "^12.0.1" + pacote "^12.0.2" + parse-conflict-json "^2.0.1" proc-log "^1.0.0" promise-all-reject-late "^1.0.0" promise-call-limit "^1.0.1" @@ -3825,23 +3820,24 @@ npm-bundled "^1.1.1" npm-normalize-package-bin "^1.0.1" -"@npmcli/map-workspaces@^1.0.2": - version "1.0.4" - resolved "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-1.0.4.tgz#915708b55afa25e20bc2c14a766c124c2c5d4cab" - integrity sha512-wVR8QxhyXsFcD/cORtJwGQodeeaDf0OxcHie8ema4VgFeqwYkFsDPnSrIRSytX8xR6nKPAH89WnwTcaU608b/Q== +"@npmcli/map-workspaces@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-2.0.0.tgz#e342efbbdd0dad1bba5d7723b674ca668bf8ac5a" + integrity sha512-QBJfpCY1NOAkkW3lFfru9VTdqvMB2TN0/vrevl5xBCv5Fi0XDVcA6rqqSau4Ysi4Iw3fBzyXV7hzyTBDfadf7g== dependencies: "@npmcli/name-from-folder" "^1.0.1" glob "^7.1.6" minimatch "^3.0.4" read-package-json-fast "^2.0.1" -"@npmcli/metavuln-calculator@^1.1.0": - version "1.1.1" - resolved "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-1.1.1.tgz#2f95ff3c6d88b366dd70de1c3f304267c631b458" - integrity sha512-9xe+ZZ1iGVaUovBVFI9h3qW+UuECUzhvZPxK9RaEA2mjU26o5D0JloGYWwLYvQELJNmBdQB6rrpuN8jni6LwzQ== +"@npmcli/metavuln-calculator@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-2.0.0.tgz#70937b8b5a5cad5c588c8a7b38c4a8bd6f62c84c" + integrity sha512-VVW+JhWCKRwCTE+0xvD6p3uV4WpqocNYYtzyvenqL/u1Q3Xx6fGTJ+6UoIoii07fbuEO9U3IIyuGY0CYHDv1sg== dependencies: cacache "^15.0.5" - pacote "^11.1.11" + json-parse-even-better-errors "^2.3.1" + pacote "^12.0.0" semver "^7.3.2" "@npmcli/move-file@^1.0.1": @@ -3864,10 +3860,10 @@ resolved "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-1.0.1.tgz#77ecd0a4fcb772ba6fe927e2e2e155fbec2e6b1a" integrity sha512-qq3oEfcLFwNfEYOQ8HLimRGKlD8WSeGEdtUa7hmzpR8Sa7haL1KVQrvgO6wqMjhWFFVjgtrh1gIxDz+P8sjUaA== -"@npmcli/node-gyp@^1.0.1", "@npmcli/node-gyp@^1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.2.tgz#3cdc1f30e9736dbc417373ed803b42b1a0a29ede" - integrity sha512-yrJUe6reVMpktcvagumoqD9r08fH1iRo01gn1u0zoCApa9lnZGEigVKUd2hzsCId4gdtkZZIVscLhNxMECKgRg== +"@npmcli/node-gyp@^1.0.2", "@npmcli/node-gyp@^1.0.3": + version "1.0.3" + resolved "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz#a912e637418ffc5f2db375e93b85837691a43a33" + integrity sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA== "@npmcli/package-json@^1.0.1": version "1.0.1" @@ -3895,6 +3891,45 @@ puka "^1.0.1" read-package-json-fast "^2.0.1" +"@npmcli/run-script@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@npmcli/run-script/-/run-script-2.0.0.tgz#9949c0cab415b17aaac279646db4f027d6f1e743" + integrity sha512-fSan/Pu11xS/TdaTpTB0MRn9guwGU8dye+x56mEVgBEd/QsybBbYcAL0phPXi8SGWFEChkQd6M9qL4y6VOpFig== + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + node-gyp "^8.2.0" + read-package-json-fast "^2.0.1" + +"@octokit/app@^12.0.4": + version "12.0.5" + resolved "https://registry.npmjs.org/@octokit/app/-/app-12.0.5.tgz#0b25446daffcb36967b26944410eab1ccbba0c06" + integrity sha512-lM3pIfx2h+UbvsXHFVs1ApJ9Rmp8LO4ciFSr5q/9MdHmhsH6WtwayieUn875xwB77IoR9r8czxxxASu2WCtdeA== + dependencies: + "@octokit/auth-app" "^3.3.0" + "@octokit/auth-unauthenticated" "^2.0.4" + "@octokit/core" "^3.4.0" + "@octokit/oauth-app" "^3.3.2" + "@octokit/plugin-paginate-rest" "^2.13.3" + "@octokit/types" "^6.27.1" + "@octokit/webhooks" "^9.0.1" + +"@octokit/auth-app@^3.3.0": + version "3.6.1" + resolved "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-3.6.1.tgz#aa5b02cc211175cbc28ce6c03c73373c1206d632" + integrity sha512-6oa6CFphIYI7NxxHrdVOzhG7hkcKyGyYocg7lNDSJVauVOLtylg8hNJzoUyPAYKKK0yUeoZamE/lMs2tG+S+JA== + dependencies: + "@octokit/auth-oauth-app" "^4.3.0" + "@octokit/auth-oauth-user" "^1.2.3" + "@octokit/request" "^5.6.0" + "@octokit/request-error" "^2.1.0" + "@octokit/types" "^6.0.3" + "@types/lru-cache" "^5.1.0" + deprecation "^2.3.1" + lru-cache "^6.0.0" + universal-github-app-jwt "^1.0.1" + universal-user-agent "^6.0.0" + "@octokit/auth-app@^3.4.0": version "3.4.0" resolved "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-3.4.0.tgz#af9f68512e7b8dd071b49e1470a1ddf88ff6a3a3" @@ -3911,6 +3946,19 @@ universal-github-app-jwt "^1.0.1" universal-user-agent "^6.0.0" +"@octokit/auth-oauth-app@^4.0.0", "@octokit/auth-oauth-app@^4.3.0": + version "4.3.0" + resolved "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-4.3.0.tgz#de02f184360ffd7cfccef861053784fc4410e7ea" + integrity sha512-cETmhmOQRHCz6cLP7StThlJROff3A/ln67Q961GuIr9zvyFXZ4lIJy9RE6Uw5O7D8IXWPU3jhDnG47FTSGQr8Q== + dependencies: + "@octokit/auth-oauth-device" "^3.1.1" + "@octokit/auth-oauth-user" "^1.2.1" + "@octokit/request" "^5.3.0" + "@octokit/types" "^6.0.3" + "@types/btoa-lite" "^1.0.0" + btoa-lite "^1.0.0" + universal-user-agent "^6.0.0" + "@octokit/auth-oauth-app@^4.1.0": version "4.1.2" resolved "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-4.1.2.tgz#bf3ff30c260e6e9f10b950386f279befb8fe907d" @@ -3946,6 +3994,18 @@ btoa-lite "^1.0.0" universal-user-agent "^6.0.0" +"@octokit/auth-oauth-user@^1.3.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-1.3.0.tgz#da4e4529145181a6aa717ae858afb76ebd6e3360" + integrity sha512-3QC/TAdk7onnxfyZ24BnJRfZv8TRzQK7SEFUS9vLng4Vv6Hv6I64ujdk/CUkREec8lhrwU764SZ/d+yrjjqhaQ== + dependencies: + "@octokit/auth-oauth-device" "^3.1.1" + "@octokit/oauth-methods" "^1.1.0" + "@octokit/request" "^5.4.14" + "@octokit/types" "^6.12.2" + btoa-lite "^1.0.0" + universal-user-agent "^6.0.0" + "@octokit/auth-token@^2.4.4": version "2.4.4" resolved "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.4.tgz#ee31c69b01d0378c12fd3ffe406030f3d94d3b56" @@ -3953,6 +4013,14 @@ dependencies: "@octokit/types" "^6.0.0" +"@octokit/auth-unauthenticated@^2.0.0", "@octokit/auth-unauthenticated@^2.0.4": + version "2.1.0" + resolved "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-2.1.0.tgz#ef97de366836e09f130de4e2205be955f9cf131c" + integrity sha512-+baofLfSL0CAv3CfGQ9rxiZZQEX8VNJMGuuS4PgrMRBUL52Ho5+hQYb63UJQshw7EXYMPDZxbXznc0y33cbPqw== + dependencies: + "@octokit/request-error" "^2.1.0" + "@octokit/types" "^6.0.3" + "@octokit/core@^3.2.3": version "3.2.4" resolved "https://registry.npmjs.org/@octokit/core/-/core-3.2.4.tgz#5791256057a962eca972e31818f02454897fd106" @@ -3965,7 +4033,7 @@ before-after-hook "^2.1.0" universal-user-agent "^6.0.0" -"@octokit/core@^3.5.1": +"@octokit/core@^3.3.2", "@octokit/core@^3.4.0", "@octokit/core@^3.5.1": version "3.5.1" resolved "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== @@ -3996,6 +4064,26 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" +"@octokit/oauth-app@^3.3.2", "@octokit/oauth-app@^3.5.1": + version "3.6.0" + resolved "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-3.6.0.tgz#36f660c7eb6b5a5cd23f6207a8d95e74b6834db0" + integrity sha512-OxPw4ItQXaC2GuEXyZB7EmZ2rHvNFX4y3yAsqdFIRW7qg2HyoEPxacxza6c8wqbEEvu84b98AJ5BXm+IjPWrww== + dependencies: + "@octokit/auth-oauth-app" "^4.0.0" + "@octokit/auth-oauth-user" "^1.3.0" + "@octokit/auth-unauthenticated" "^2.0.0" + "@octokit/core" "^3.3.2" + "@octokit/oauth-authorization-url" "^4.2.1" + "@octokit/oauth-methods" "^1.2.2" + "@types/aws-lambda" "^8.10.83" + fromentries "^1.3.1" + universal-user-agent "^6.0.0" + +"@octokit/oauth-authorization-url@^4.2.1": + version "4.3.3" + resolved "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-4.3.3.tgz#6a6ef38f243086fec882b62744f39b517528dfb9" + integrity sha512-lhP/t0i8EwTmayHG4dqLXgU+uPVys4WD/qUNvC+HfB1S1dyqULm5Yx9uKc1x79aP66U1Cb4OZeW8QU/RA9A4XA== + "@octokit/oauth-authorization-url@^4.3.1": version "4.3.1" resolved "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-4.3.1.tgz#008d09bf427a7f61c70b5283040d60a456011a51" @@ -4012,6 +4100,17 @@ "@octokit/types" "^6.12.2" btoa-lite "^1.0.0" +"@octokit/oauth-methods@^1.2.2": + version "1.2.6" + resolved "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-1.2.6.tgz#b9ac65e374b2cc55ee9dd8dcdd16558550438ea7" + integrity sha512-nImHQoOtKnSNn05uk2o76om1tJWiAo4lOu2xMAHYsNr0fwopP+Dv+2MlGvaMMlFjoqVd3fF3X5ZDTKCsqgmUaQ== + dependencies: + "@octokit/oauth-authorization-url" "^4.3.1" + "@octokit/request" "^5.4.14" + "@octokit/request-error" "^2.0.5" + "@octokit/types" "^6.12.2" + btoa-lite "^1.0.0" + "@octokit/openapi-types@^11.2.0": version "11.2.0" resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6" @@ -4027,7 +4126,7 @@ resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== -"@octokit/plugin-paginate-rest@^2.16.8": +"@octokit/plugin-paginate-rest@^2.13.3", "@octokit/plugin-paginate-rest@^2.16.8": version "2.17.0" resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz#32e9c7cab2a374421d3d0de239102287d791bce7" integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw== @@ -4067,6 +4166,22 @@ "@octokit/types" "^6.34.0" deprecation "^2.3.1" +"@octokit/plugin-retry@^3.0.9": + version "3.0.9" + resolved "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-3.0.9.tgz#ae625cca1e42b0253049102acd71c1d5134788fe" + integrity sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ== + dependencies: + "@octokit/types" "^6.0.3" + bottleneck "^2.15.3" + +"@octokit/plugin-throttling@^3.5.1": + version "3.5.2" + resolved "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-3.5.2.tgz#8b1797a5f14edbca0b8af619394056ed0ed5c9b5" + integrity sha512-Eu7kfJxU8vmHqWGNszWpg+GVp2tnAfax3XQV5CkYPEE69C+KvInJXW9WajgSeW+cxYe0UVdouzCtcreGNuJo7A== + dependencies: + "@octokit/types" "^6.0.1" + bottleneck "^2.15.3" + "@octokit/request-error@^2.0.0", "@octokit/request-error@^2.0.2", "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": version "2.1.0" resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" @@ -4122,7 +4237,7 @@ dependencies: "@octokit/openapi-types" "^7.3.2" -"@octokit/types@^6.34.0": +"@octokit/types@^6.26.0", "@octokit/types@^6.27.1", "@octokit/types@^6.34.0": version "6.34.0" resolved "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz#c6021333334d1ecfb5d370a8798162ddf1ae8218" integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw== @@ -4139,7 +4254,7 @@ resolved "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-5.2.0.tgz#9d1d451f37460107409c81cab04dd473108abb02" integrity sha512-OZhKy1w8/GF4GWtdiJc+o8sloWAHRueGB78FWFLZnueK7EHV9MzDVr4weJZMflJwMK4uuYLzcnJVnAoy3yB35g== -"@octokit/webhooks@^9.14.1": +"@octokit/webhooks@^9.0.1", "@octokit/webhooks@^9.14.1": version "9.22.0" resolved "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-9.22.0.tgz#07a36a10358d39c1870758fae2b1ad3c24ca578d" integrity sha512-wUd7nGfDRHG6xkz311djmq6lIB2tQ+r94SNkyv9o0bQhOsrkwH8fQCM7uVsbpkGUU2lqCYsVoa8z/UC9HJgRaw== @@ -4308,13 +4423,13 @@ react-use "^17.2.4" "@roadiehq/backstage-plugin-github-pull-requests@^1.3.2": - version "1.3.4" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-1.3.4.tgz#3e6be41fe81909b28a3d517aadcc7ae6b70d7d7a" - integrity sha512-F1y3CmMZOiNiam3zt6TBaJ5+NqAhPwS3HkWbUsMvJq1PKAzYdSeDBPhoTBwhYOu3uV36EtKjfog4r4wVHOf7EA== + version "1.3.7" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-1.3.7.tgz#d3c884c9b85f88c5f851f383596a616598c56076" + integrity sha512-a3PBLQ6SRtEDGNIekcGT8v4Aaac3BoEww/UU9uaQuE7bCEVolx2MeOVbSxb1uCWr6C6uGeDF5Kw4S4hPJJcnNg== dependencies: "@backstage/catalog-model" "^0.9.7" "@backstage/core-components" "^0.8.0" - "@backstage/core-plugin-api" "^0.4.0" + "@backstage/core-plugin-api" "^0.6.0" "@backstage/plugin-catalog-react" "^0.6.5" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -4328,13 +4443,13 @@ react-use "^17.2.4" "@roadiehq/backstage-plugin-travis-ci@^1.3.2": - version "1.3.3" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-travis-ci/-/backstage-plugin-travis-ci-1.3.3.tgz#317c9280d8dd404794ad5aa057ce457bfd459706" - integrity sha512-f5RhuG681iwiRa9/WvwhV4pI0KHv6oAHlgOXG3/pbOcONN9w9RaQIITgTyc6hpm9MzAkFQWV8qc3yPFcojza+A== + version "1.3.6" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-travis-ci/-/backstage-plugin-travis-ci-1.3.6.tgz#6f45a42bdf39aa6baab56f0c4400990238df1625" + integrity sha512-th+2GaOjkPArDUbTBuhhCz/6WL4gcrVRVAsDHzuSrrJqNtsz6kXdBIHhR/58En16BZ2bnHhRzY87uiUDLjAntA== dependencies: "@backstage/catalog-model" "^0.9.7" "@backstage/core-components" "^0.8.0" - "@backstage/core-plugin-api" "^0.4.0" + "@backstage/core-plugin-api" "^0.6.0" "@backstage/plugin-catalog-react" "^0.6.5" "@backstage/theme" "^0.2.9" "@material-ui/core" "^4.11.3" @@ -4919,6 +5034,11 @@ resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== +"@types/aws-lambda@^8.10.83": + version "8.10.92" + resolved "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.92.tgz#645f769ff88b8eba1acd35542695ac322c7757c4" + integrity sha512-dB14TltT1SNq73z3MaZfKyyBZ37NAgAFl8jze59bisR4fJ6pB6AYGxItHFkooZbN7UcVJX/cFudM4p8wp1W4rA== + "@types/aws4@^1.5.1": version "1.5.2" resolved "https://registry.npmjs.org/@types/aws4/-/aws4-1.5.2.tgz#34e35b4405a619b9205be3e7678963bc7c8a47db" @@ -4957,15 +5077,7 @@ dependencies: "@babel/types" "^7.3.0" -"@types/body-parser@*", "@types/body-parser@^1.19.0": - version "1.19.1" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz#0c0174c42a7d017b818303d4b5d969cb0b75929c" - integrity sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/body-parser@1.19.2": +"@types/body-parser@*", "@types/body-parser@1.19.2", "@types/body-parser@^1.19.0": version "1.19.2" resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== @@ -5052,10 +5164,10 @@ dependencies: "@types/express" "*" -"@types/concat-stream@^1.6.0": - version "1.6.1" - resolved "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz#24bcfc101ecf68e886aaedce60dfd74b632a1b74" - integrity sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA== +"@types/concat-stream@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-2.0.0.tgz#a716f0ba9015014e643addb351da05a73bef425c" + integrity sha512-t3YCerNM7NTVjLuICZo5gYAXYoDvpuuTceCcFQWcDQz26kxUR5uIWolxbIR5jRNIXpMqhOpW/b8imCR1LEmuJw== dependencies: "@types/node" "*" @@ -5113,6 +5225,11 @@ resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.0.2.tgz#53f2d6325f66ee79afd707c05ac849e8ae0edbb0" integrity sha512-WVx6zBiz4sWlboCy7TCgjeyHpNjMsoF36yaagny1uXfbadc9f+5BeBf7U+lRmQqY3EHbGQpP8UdW8AC+cywSwQ== +"@types/d3-color@^2": + version "2.0.3" + resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.3.tgz#8bc4589073c80e33d126345542f588056511fe82" + integrity sha512-+0EtEjBfKEDtH9Rk3u3kLOUXM5F+iZK+WvASPb0MhIZl8J8NUvGeZRwKCXl+P3HkYx5TdU4YtcibpqHkSR9n7w== + "@types/d3-force@^2.1.1": version "2.1.1" resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-2.1.1.tgz#a18b6f029d056eb0f8f84a09471e6228e4469b14" @@ -5125,6 +5242,13 @@ dependencies: "@types/d3-color" "*" +"@types/d3-interpolate@^2.0.0": + version "2.0.2" + resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.2.tgz#78eddf7278b19e48e8652603045528d46897aba0" + integrity sha512-lElyqlUfIPyWG/cD475vl6msPL4aMU7eJvx1//Q177L8mdXoVPFl1djIESF2FKnc0NyaHvQlJpWwKJYwAhUoCw== + dependencies: + "@types/d3-color" "^2" + "@types/d3-path@*": version "3.0.0" resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" @@ -5135,6 +5259,18 @@ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.9.tgz#73526b150d14cd96e701597cbf346cfd1fd4a58c" integrity sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ== +"@types/d3-path@^2": + version "2.0.1" + resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-2.0.1.tgz#ca03dfa8b94d8add97ad0cd97e96e2006b4763cb" + integrity sha512-6K8LaFlztlhZO7mwsZg7ClRsdLg3FJRzIIi6SZXDWmmSJc2x8dd2VkESbLXdk3p8cuvz71f36S0y8Zv2AxqvQw== + +"@types/d3-scale@^3.0.0": + version "3.3.2" + resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-3.3.2.tgz#18c94e90f4f1c6b1ee14a70f14bfca2bd1c61d06" + integrity sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ== + dependencies: + "@types/d3-time" "^2" + "@types/d3-selection@*", "@types/d3-selection@^3.0.1": version "3.0.2" resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.2.tgz#23e48a285b24063630bbe312cc0cfe2276de4a59" @@ -5147,6 +5283,13 @@ dependencies: "@types/d3-path" "^1" +"@types/d3-shape@^2.0.0": + version "2.1.3" + resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-2.1.3.tgz#35d397b9e687abaa0de82343b250b9897b8cacf3" + integrity sha512-HAhCel3wP93kh4/rq+7atLdybcESZ5bRHDEZUojClyZWsRuEMo3A52NGYJSh48SxfxEU6RZIVbZL2YFZ2OAlzQ== + dependencies: + "@types/d3-path" "^2" + "@types/d3-shape@^3.0.1": version "3.0.2" resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz#4b1ca4ddaac294e76b712429726d40365cd1e8ca" @@ -5154,6 +5297,11 @@ dependencies: "@types/d3-path" "*" +"@types/d3-time@^2": + version "2.1.1" + resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-2.1.1.tgz#743fdc821c81f86537cbfece07093ac39b4bc342" + integrity sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg== + "@types/d3-zoom@^3.0.1": version "3.0.1" resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826" @@ -5188,24 +5336,17 @@ "@types/ssh2" "*" "@types/dockerode@^3.2.5", "@types/dockerode@^3.3.0": - version "3.3.1" - resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.1.tgz#4b7e1b3c2b8636c3ef4f2628ca42d1d4bf78811d" - integrity sha512-pjEOl3C/N2viC+nU2iEM6ImFcyu/RoLZvfsAGc3xIW2MlyEox+ZtJgPDXCFfFVh8ZNouxCRL673aa2Ywbx20EQ== + version "3.3.3" + resolved "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.3.tgz#993de5d8bd3ecb0b0a9277cccc189e9a74e9fbd1" + integrity sha512-DBFTE/wiZKodQhDrdkzLVT3K0X1JO/1fTxPaTQN0jiPNdTOFoPt8LYnuYEzalPgVpZbGEeuJTls7qn6jMvuydg== dependencies: "@types/docker-modem" "*" "@types/node" "*" -"@types/dompurify@^2.1.0": - version "2.3.1" - resolved "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.3.1.tgz#2934adcd31c4e6b02676f9c22f9756e5091c04dd" - integrity sha512-YJth9qa0V/E6/XPH1Jq4BC8uCMmO8V1fKWn8PCvuZcAhMn7q0ez9LW6naQT04UZzjFfAPhyRMZmI2a2rbMlEFA== - dependencies: - "@types/trusted-types" "*" - -"@types/dompurify@^2.2.2": - version "2.2.3" - resolved "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.2.3.tgz#6e89677a07902ac1b6821c345f34bd85da239b08" - integrity sha512-CLtc2mZK8+axmrz1JqtpklO/Kvn38arGc8o1l3UVopZaXXuer9ONdZwJ/9f226GrhRLtUmLr9WrvZsRSNpS8og== +"@types/dompurify@^2.1.0", "@types/dompurify@^2.2.2": + version "2.3.3" + resolved "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.3.3.tgz#c24c92f698f77ed9cc9d9fa7888f90cf2bfaa23f" + integrity sha512-nnVQSgRVuZ/843oAfhA25eRSNzUFcBPk/LOiw5gm8mD9/X7CNcbRkQu/OsjCewO8+VIYfPxUnXvPEVGenw14+w== dependencies: "@types/trusted-types" "*" @@ -5249,9 +5390,9 @@ integrity sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18", "@types/express-serve-static-core@^4.17.5": - version "4.17.24" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07" - integrity sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA== + version "4.17.28" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" + integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== dependencies: "@types/node" "*" "@types/qs" "*" @@ -5281,17 +5422,7 @@ "@types/express" "*" "@types/xml2js" "*" -"@types/express@*", "@types/express@^4.17.6": - version "4.17.12" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz#4bc1bf3cd0cfe6d3f6f2853648b40db7d54de350" - integrity sha512-pTYas6FrP15B1Oa0bkN5tQMNqOcVXa9j4FTFtO8DWI9kppKib+6NJtfTOOLcwxuuYvcX2+dVG6et1SxW/Kc17Q== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.18" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/express@4.17.13": +"@types/express@*", "@types/express@4.17.13", "@types/express@^4.17.6": version "4.17.13" resolved "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -5419,6 +5550,13 @@ "@types/through" "*" rxjs "^7.2.0" +"@types/is-ci@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.0.tgz#7e8910af6857601315592436f030aaa3ed9783c3" + integrity sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ== + dependencies: + ci-info "^3.1.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.1" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -5477,12 +5615,7 @@ resolved "https://registry.npmjs.org/@types/js-levenshtein/-/js-levenshtein-1.1.0.tgz#9541eec4ad6e3ec5633270a3a2b55d981edc44a9" integrity sha512-14t0v1ICYRtRVcHASzes0v/O+TIeASb8aD55cWF1PidtInhFWSXcmhzhHqGjUWf9SUq1w70cvd1cWKUULubAfQ== -"@types/js-yaml@^4.0.0": - version "4.0.1" - resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.1.tgz#5544730b65a480b18ace6b6ce914e519cec2d43b" - integrity sha512-xdOvNmXmrZqqPy3kuCQ+fz6wA0xU5pji9cd1nDrflWaAWtYLLGk5ykW0H6yg5TVyehHP1pfmuuSaZkhP+kspVA== - -"@types/js-yaml@^4.0.1": +"@types/js-yaml@^4.0.0", "@types/js-yaml@^4.0.1": version "4.0.5" resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138" integrity sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== @@ -5681,6 +5814,11 @@ resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/npm-packlist@^1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@types/npm-packlist/-/npm-packlist-1.1.2.tgz#285978c9023ce68fa0641ca606c7c3b7b0e851c5" + integrity sha512-9NYoEH87t90e6dkaQOuUTY/R1xUE0a67sXzJBuAB+b+/z4FysHFD19g/O154ToGjyWqKYkezVUtuBdtfd4hyfw== + "@types/nunjucks@^3.1.4": version "3.2.1" resolved "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.1.tgz#02a3ade3dc4d3950029c6466a4034565dba7cf8c" @@ -5713,9 +5851,9 @@ "@types/passport-oauth2" "*" "@types/passport-google-oauth20@^2.0.3": - version "2.0.7" - resolved "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.7.tgz#0d97b7a886a0c0d2158682145cd949b29f8efe86" - integrity sha512-0HPVSqDmOWk5fRLb+bqGal+6iWsERiEco/Mli77yy5NEy22IfkoRoqZTSZ8UtXDWY9DCZlpS1Jqq56iWx2torw== + version "2.0.11" + resolved "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.11.tgz#271ec71de3030a3e1c004b24e633e4b298ccba97" + integrity sha512-9XMT1GfwhZL7UQEiCepLef55RNPHkbrCtsU7rsWPTEOsmu5qVIW8nSemtB4p+P24CuOhA+IKkv8LsPThYghGww== dependencies: "@types/express" "*" "@types/passport" "*" @@ -5871,9 +6009,9 @@ "@types/react" "*" "@types/react@*", "@types/react@>=16.9.0", "@types/react@^16.13.1 || ^17.0.0": - version "16.14.18" - resolved "https://registry.npmjs.org/@types/react/-/react-16.14.18.tgz#b2bcea05ee244fde92d409f91bd888ca8e54b20f" - integrity sha512-eeyqd1mqoG43mI0TvNKy9QNf1Tjz3DEOsRP3rlPo35OeMIt05I+v9RR8ZvL2GuYZeF2WAcLXJZMzu6zdz3VbtQ== + version "17.0.39" + resolved "https://registry.npmjs.org/@types/react/-/react-17.0.39.tgz#d0f4cde092502a6db00a1cded6e6bf2abb7633ce" + integrity sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -5909,6 +6047,11 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/resize-observer-browser@^0.1.6": + version "0.1.7" + resolved "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz#294aaadf24ac6580b8fbd1fe3ab7b59fe85f9ef3" + integrity sha512-G9eN0Sn0ii9PWQ3Vl72jDPgeJwRWhv2Qk/nQkJuWmRmOB4HX3/BhD5SE1dZs/hzPZL/WKnvF0RHdTSG54QJFyg== + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -5929,9 +6072,9 @@ integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== "@types/rollup-plugin-peer-deps-external@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@types/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.0.tgz#eae7d8b9d27fa037f5bcaded24e389f85b81973c" - integrity sha512-BiztED+KYJPBI3ihLSOuuZSzy836WAOpC9UrMtDqk3+VeByR8A5pJJ9pCVnq/dfB4wyCeiFL+FlyJnqZuP3pxg== + version "2.2.1" + resolved "https://registry.npmjs.org/@types/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.1.tgz#7711fe77727c2ec920d3e096fa4e61d02734a23d" + integrity sha512-CUK4Mi3M3A/0coRUeo4McvUFOxR5H6UYd6Wt3D+K4c4fJupolMSoIxsZaBG1g/BJQOaujf78Pf7KhgfhrX1FZA== dependencies: "@types/node" "*" rollup "^0.63.4" @@ -6259,9 +6402,9 @@ integrity sha512-qRyuv+P/1t1JK1rA+elmK1MmCL1BapEzKKfbEhDBV/LMMse4lmhZ/XbgETI39JveDJRpLjmToOI6uFtMW/WR2g== "@types/zen-observable@^0.8.0", "@types/zen-observable@^0.8.2": - version "0.8.2" - resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71" - integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg== + version "0.8.3" + resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3" + integrity sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw== "@typescript-eslint/eslint-plugin@^5.9.0": version "5.9.0" @@ -6377,6 +6520,13 @@ "@typescript-eslint/types" "5.9.1" eslint-visitor-keys "^3.0.0" +"@vscode/sqlite3@^5.0.7": + version "5.0.7" + resolved "https://registry.npmjs.org/@vscode/sqlite3/-/sqlite3-5.0.7.tgz#358df36bb0e9e735c54785e3e4b9b2dce1d32895" + integrity sha512-NlsOf+Hir2r4zopI1qMvzWXPwPJuFscirkmFTniTAT24Yz2FWcyZxzK7UT8iSNiTqOCPz48yF55ZVHaz7tTuVQ== + dependencies: + node-addon-api "^4.2.0" + "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -6594,7 +6744,7 @@ acorn-walk@^7.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ== -acorn-walk@^8.1.1: +acorn-walk@^8.1.1, acorn-walk@^8.2.0: version "8.2.0" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== @@ -6604,17 +6754,7 @@ acorn@^7.1.1: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.2.4: - version "8.6.0" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" - integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== - -acorn@^8.4.1: - version "8.4.1" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c" - integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA== - -acorn@^8.7.0: +acorn@^8.2.4, acorn@^8.4.1, acorn@^8.7.0: version "8.7.0" resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== @@ -6643,10 +6783,10 @@ agent-base@^6.0.2: dependencies: debug "4" -agentkeepalive@^4.1.3, agentkeepalive@^4.1.4: - version "4.1.4" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b" - integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ== +agentkeepalive@^4.1.3, agentkeepalive@^4.1.4, agentkeepalive@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.0.tgz#616ce94ccb41d1a39a45d203d8076fe98713062d" + integrity sha512-0PhAp58jZNw13UJv7NVdTGb0ZcghHUb3DrZ046JiiJY/BOaTTpbwdHq2VObPCBV8M2GPh7sgrJ3AQ8Ey468LJw== dependencies: debug "^4.1.0" depd "^1.1.2" @@ -6714,6 +6854,11 @@ alphanum-sort@^1.0.2: resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= +already@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/already/-/already-3.3.0.tgz#a5e5becd167cf537b45f8f1c23d331488ed77003" + integrity sha512-ADGyKddqEp8t/Wu4ITc0y9GGsgZDgyMeMk38AM5qrPK7VEjNAYD87QGTGGgNhSQahmjw76V3mi+3fJRwPJXcTw== + anafanafo@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/anafanafo/-/anafanafo-2.0.0.tgz#43f56274680bc553dd67a9625a920f88d0057b5c" @@ -6721,13 +6866,6 @@ anafanafo@2.0.0: dependencies: char-width-table-consumer "^1.0.0" -ansi-align@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" - integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38= - dependencies: - string-width "^2.0.0" - ansi-align@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" @@ -6946,7 +7084,7 @@ aproba@^1.0.3: resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== -aproba@^2.0.0: +"aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== @@ -6985,13 +7123,21 @@ archiver@^5.0.2, archiver@^5.3.0: tar-stream "^2.2.0" zip-stream "^4.1.0" -are-we-there-yet@^1.1.5: - version "1.1.7" - resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" - integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== dependencies: delegates "^1.0.0" - readable-stream "^2.0.6" + readable-stream "^3.6.0" + +are-we-there-yet@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz#ba20bd6b553e31d62fc8c31bd23d22b95734390d" + integrity sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" are-we-there-yet@~1.1.2: version "1.1.5" @@ -7066,11 +7212,6 @@ array-filter@^1.0.0: resolved "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -7227,10 +7368,10 @@ async@^2.6.2: dependencies: lodash "^4.17.14" -async@^3.1.0, async@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== +async@^3.2.0, async@^3.2.3: + version "3.2.3" + resolved "https://registry.npmjs.org/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== asynckit@^0.4.0: version "0.4.0" @@ -7618,17 +7759,17 @@ bignumber.js@^9.0.0: resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== -bin-links@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/bin-links/-/bin-links-2.2.1.tgz#347d9dbb48f7d60e6c11fe68b77a424bee14d61b" - integrity sha512-wFzVTqavpgCCYAh8SVBdnZdiQMxTkGR+T3b14CNpBXIBe2neJWaMGAZ55XWWHELJJ89dscuq0VCBqcVaIOgCMg== +bin-links@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/bin-links/-/bin-links-3.0.0.tgz#8273063638919f6ba4fbe890de9438c1b3adf0b7" + integrity sha512-fC7kPWcEkAWBgCKxmAMqZldlIeHsXwQy9JXzrppAVQiukGiDKxmYesJcBKWu6UMwx/5GOfo10wtK/4zy+Xt/mg== dependencies: cmd-shim "^4.0.1" - mkdirp "^1.0.3" + mkdirp-infer-owner "^2.0.0" npm-normalize-package-bin "^1.0.0" read-cmd-shim "^2.0.0" rimraf "^3.0.0" - write-file-atomic "^3.0.3" + write-file-atomic "^4.0.0" binary-extensions@^2.0.0: version "2.0.0" @@ -7672,13 +7813,6 @@ blob-util@^2.0.2: resolved "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== -block-stream@*: - version "0.0.9" - resolved "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= - dependencies: - inherits "~2.0.0" - bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -7727,32 +7861,24 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -boxen@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" - integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw== - dependencies: - ansi-align "^2.0.0" - camelcase "^4.0.0" - chalk "^2.0.1" - cli-boxes "^1.0.0" - string-width "^2.0.0" - term-size "^1.2.0" - widest-line "^2.0.0" +bottleneck@^2.15.3: + version "2.19.5" + resolved "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91" + integrity sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== -boxen@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" - integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== +boxen@^5.0.0: + version "5.1.2" + resolved "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== dependencies: ansi-align "^3.0.0" - camelcase "^5.3.1" - chalk "^3.0.0" - cli-boxes "^2.2.0" - string-width "^4.1.0" - term-size "^2.1.0" - type-fest "^0.8.1" + camelcase "^6.2.0" + chalk "^4.1.0" + cli-boxes "^2.2.1" + string-width "^4.2.2" + type-fest "^0.20.2" widest-line "^3.1.0" + wrap-ansi "^7.0.0" brace-expansion@^1.1.7: version "1.1.11" @@ -7984,17 +8110,12 @@ bytes@3.0.0: resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== - bytes@3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== -cacache@^15.0.3, cacache@^15.0.5, cacache@^15.2.0: +cacache@^15.0.3, cacache@^15.0.5, cacache@^15.2.0, cacache@^15.3.0: version "15.3.0" resolved "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== @@ -8095,14 +8216,6 @@ camel-case@4.1.2, camel-case@^4.1.2: pascal-case "^3.1.2" tslib "^2.0.3" -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -8112,26 +8225,26 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" +camelcase-keys@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.1.tgz#5a57e6dfb3f6c7929dad15599ee4476a7e9a3b2d" + integrity sha512-P331lEls98pW8JLyodNWfzuz91BEDVA4VpW2/SwXnyv2K495tq1N777xzDbFgnEigfA7UIY0xa6PwR/H9jijjA== + dependencies: + camelcase "^6.2.0" + map-obj "^4.1.0" + quick-lru "^5.1.1" + type-fest "^1.2.1" + camelcase@5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== -camelcase@^2.0.0: - version "2.1.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" - integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= - camelcase@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= -camelcase@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" - integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= - camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -8204,7 +8317,7 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -8339,10 +8452,10 @@ check-types@^11.1.1: resolved "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== -chokidar@^3.2.2, chokidar@^3.3.1, chokidar@^3.4.2, chokidar@^3.5.2: - version "3.5.2" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== +chokidar@^3.3.1, chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -8376,10 +8489,10 @@ ci-info@^2.0.0: resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -ci-info@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.1.1.tgz#9a32fcefdf7bcdb6f0a7e1c0f8098ec57897b80a" - integrity sha512-kdRWLBIJwdsYJWYJFtAFFYxybguqeF91qpZaggjG5Nf8QKdizFG2hjqvaTXbxFIcYbSaD74KpAXv6BSm17DHEQ== +ci-info@^3.1.0, ci-info@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" + integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" @@ -8433,15 +8546,10 @@ clean-stack@^2.0.0: resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cli-boxes@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" - integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM= - -cli-boxes@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" - integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== cli-cursor@^1.0.2: version "1.0.2" @@ -8696,6 +8804,11 @@ color-string@^1.5.2, color-string@^1.6.0: color-name "^1.0.0" simple-swizzle "^0.2.2" +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + color@3.0.x: version "3.0.0" resolved "https://registry.npmjs.org/color/-/color-3.0.0.tgz#d920b4328d534a3ac8295d68f7bd4ba6c427be9a" @@ -8717,12 +8830,7 @@ colord@^2.9.1: resolved "https://registry.npmjs.org/colord/-/colord-2.9.1.tgz#c961ea0efeb57c9f0f4834458f26cb9cc4a3f90e" integrity sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw== -colorette@1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== - -colorette@^2.0.10, colorette@^2.0.16: +colorette@2.0.16, colorette@^2.0.10, colorette@^2.0.16: version "2.0.16" resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== @@ -8732,7 +8840,7 @@ colors@1.0.3: resolved "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= -colors@1.4.0, colors@^1.1.2, colors@^1.2.1: +colors@1.4.0, colors@^1.1.2: version "1.4.0" resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -8810,7 +8918,7 @@ commander@^6.1.0: resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== -commander@^7.1.0, commander@^7.2.0: +commander@^7.2.0: version "7.2.0" resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== @@ -8973,7 +9081,7 @@ console-browserify@^1.1.0: resolved "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== -console-control-strings@^1.0.0, console-control-strings@~1.1.0: +console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= @@ -9018,15 +9126,15 @@ conventional-changelog-angular@^5.0.12: q "^1.5.1" conventional-changelog-core@^4.2.2: - version "4.2.2" - resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.2.tgz#f0897df6d53b5d63dec36b9442bd45354f8b3ce5" - integrity sha512-7pDpRUiobQDNkwHyJG7k9f6maPo9tfPzkSWbRq97GGiZqisElhnvUZSvyQH20ogfOjntB5aadvv6NNcKL1sReg== + version "4.2.4" + resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz#e50d047e8ebacf63fac3dc67bf918177001e1e9f" + integrity sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg== dependencies: add-stream "^1.0.0" - conventional-changelog-writer "^4.0.18" + conventional-changelog-writer "^5.0.0" conventional-commits-parser "^3.2.0" dateformat "^3.0.0" - get-pkg-repo "^1.0.0" + get-pkg-repo "^4.0.0" git-raw-commits "^2.0.8" git-remote-origin-url "^2.0.0" git-semver-tags "^4.1.1" @@ -9035,7 +9143,6 @@ conventional-changelog-core@^4.2.2: q "^1.5.1" read-pkg "^3.0.0" read-pkg-up "^3.0.0" - shelljs "^0.8.3" through2 "^4.0.0" conventional-changelog-preset-loader@^2.3.4: @@ -9043,15 +9150,14 @@ conventional-changelog-preset-loader@^2.3.4: resolved "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== -conventional-changelog-writer@^4.0.18: - version "4.1.0" - resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz#1ca7880b75aa28695ad33312a1f2366f4b12659f" - integrity sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw== +conventional-changelog-writer@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz#e0757072f045fe03d91da6343c843029e702f359" + integrity sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ== dependencies: - compare-func "^2.0.0" conventional-commits-filter "^2.0.7" dateformat "^3.0.0" - handlebars "^4.7.6" + handlebars "^4.7.7" json-stringify-safe "^5.0.1" lodash "^4.17.15" meow "^8.0.0" @@ -9282,9 +9388,9 @@ create-require@^1.1.0: integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== cronstrue@^1.122.0: - version "1.122.0" - resolved "https://registry.npmjs.org/cronstrue/-/cronstrue-1.122.0.tgz#bd6838077b476d28f61d381398b47b8c3912a126" - integrity sha512-PFuhZd+iPQQ0AWTXIEYX+t3nFGzBrWxmTWUKJOrsGRewaBSLKZ4I1f8s2kryU75nNxgyugZgiGh2OJsCTA/XlA== + version "1.123.0" + resolved "https://registry.npmjs.org/cronstrue/-/cronstrue-1.123.0.tgz#de622dd8ea07981790f488d3f89a25e6e728ac8b" + integrity sha512-hVu9yNYRYr+jj5KET1p7FaBxFwtCHM1ByffP9lZ6yJ6p53u4VEmzH8117v9PUydxWNzc8Eq+sCZEzsKcB3ckiA== cross-env@^7.0.0: version "7.0.3" @@ -9300,14 +9406,14 @@ cross-fetch@3.1.4: dependencies: node-fetch "2.6.1" -cross-fetch@^3.0.4, cross-fetch@^3.0.6, cross-fetch@^3.1.3, cross-fetch@^3.1.4: +cross-fetch@^3.0.4, cross-fetch@^3.0.6, cross-fetch@^3.1.3, cross-fetch@^3.1.4, cross-fetch@^3.1.5: version "3.1.5" resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== dependencies: node-fetch "2.6.7" -cross-spawn@^5.0.1, cross-spawn@^5.1.0: +cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= @@ -9484,7 +9590,7 @@ css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" -css-unit-converter@^1.1.2: +css-unit-converter@^1.1.1, css-unit-converter@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA== @@ -9647,13 +9753,6 @@ csv@^5.3.1: csv-stringify "^5.3.6" stream-transform "^2.0.1" -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - cypress@^7.3.0: version "7.3.0" resolved "https://registry.npmjs.org/cypress/-/cypress-7.3.0.tgz#17345b8d18681c120f033e7d8fd0f0271e9d0d51" @@ -9699,6 +9798,13 @@ cypress@^7.3.0: url "^0.11.0" yauzl "^2.10.0" +d3-array@2, d3-array@^2.3.0: + version "2.12.1" + resolved "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81" + integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ== + dependencies: + internmap "^1.0.0" + d3-array@^1.2.0: version "1.2.4" resolved "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" @@ -9714,6 +9820,11 @@ d3-color@1: resolved "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a" integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q== +"d3-color@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" + integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== + "d3-color@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.0.1.tgz#03316e595955d1fcd39d9f3610ad41bb90194d0a" @@ -9756,6 +9867,11 @@ d3-format@1: resolved "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4" integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ== +"d3-format@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" + integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== + d3-interpolate@1, d3-interpolate@^1.3.0: version "1.4.0" resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" @@ -9770,11 +9886,23 @@ d3-interpolate@1, d3-interpolate@^1.3.0: dependencies: d3-color "1 - 3" +"d3-interpolate@1.2.0 - 2", d3-interpolate@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz#98be499cfb8a3b94d4ff616900501a64abc91163" + integrity sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ== + dependencies: + d3-color "1 - 2" + d3-path@1: version "1.0.9" resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf" integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg== +"d3-path@1 - 2": + version "2.0.0" + resolved "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz#55d86ac131a0548adae241eebfb56b4582dd09d8" + integrity sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA== + "d3-path@1 - 3": version "3.0.1" resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" @@ -9797,6 +9925,17 @@ d3-scale@^2.1.0: d3-time "1" d3-time-format "2" +d3-scale@^3.0.0: + version "3.3.0" + resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz#28c600b29f47e5b9cd2df9749c206727966203f3" + integrity sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ== + dependencies: + d3-array "^2.3.0" + d3-format "1 - 2" + d3-interpolate "1.2.0 - 2" + d3-time "^2.1.1" + d3-time-format "2 - 3" + "d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" @@ -9809,6 +9948,13 @@ d3-shape@^1.2.0: dependencies: d3-path "1" +d3-shape@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz#3b6a82ccafbc45de55b57fcf956c584ded3b666f" + integrity sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA== + dependencies: + d3-path "1 - 2" + d3-shape@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556" @@ -9823,11 +9969,25 @@ d3-time-format@2: dependencies: d3-time "1" +"d3-time-format@2 - 3": + version "3.0.0" + resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz#df8056c83659e01f20ac5da5fdeae7c08d5f1bb6" + integrity sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag== + dependencies: + d3-time "1 - 2" + d3-time@1: version "1.1.0" resolved "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== +"d3-time@1 - 2", d3-time@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz#e9d8a8a88691f4548e68ca085e5ff956724a6682" + integrity sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ== + dependencies: + d3-array "2" + "d3-timer@1 - 2": version "2.0.0" resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz#055edb1d170cfe31ab2da8968deee940b56623e6" @@ -9949,20 +10109,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3: +debug@4, debug@4.3.3, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3: version "4.3.3" resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" -debug@4.3.1: - version "4.3.1" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - debug@4.3.2: version "4.3.2" resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" @@ -9970,7 +10123,7 @@ debug@4.3.2: dependencies: ms "2.1.2" -debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: +debug@^3.1.0, debug@^3.1.1, debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -9990,7 +10143,7 @@ decamelize-keys@^1.1.0: decamelize "^1.1.0" map-obj "^1.0.0" -decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -10215,7 +10368,7 @@ detect-indent@^6.0.0: resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== -detect-libc@^1.0.2, detect-libc@^1.0.3: +detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= @@ -10448,11 +10601,16 @@ domhandler@^4.2.0: dependencies: domelementtype "^2.2.0" -dompurify@=2.3.3, dompurify@^2.2.7, dompurify@^2.2.9: +dompurify@=2.3.3: version "2.3.3" resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c" integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg== +dompurify@^2.2.7, dompurify@^2.2.9: + version "2.3.5" + resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.5.tgz#c83ed5a3ae5ce23e52efe654ea052ffb358dd7e3" + integrity sha512-kD+f8qEaa42+mjdOpKeztu9Mfx5bv9gVLO6K9jRx4uGvh6Wv06Srn4jr1wPNY2OOUGGSKHNFN+A8MA3v0E0QAQ== + domutils@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" @@ -10604,30 +10762,6 @@ elliptic@^6.0.0: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -"embedded-techdocs-app@file:packages/embedded-techdocs-app": - version "0.2.61" - dependencies: - "@backstage/app-defaults" "^0.1.5" - "@backstage/catalog-model" "^0.9.10" - "@backstage/cli" "^0.13.0" - "@backstage/config" "^0.1.13" - "@backstage/core-app-api" "^0.5.1" - "@backstage/core-components" "^0.8.6" - "@backstage/core-plugin-api" "^0.6.0" - "@backstage/integration-react" "^0.1.19" - "@backstage/plugin-catalog" "^0.7.10" - "@backstage/plugin-techdocs" "^0.13.1" - "@backstage/test-utils" "^0.2.3" - "@backstage/theme" "^0.2.14" - "@material-ui/core" "^4.11.0" - "@material-ui/icons" "^4.9.1" - history "^5.0.0" - react "^17.0.2" - react-dom "^17.0.2" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-use "^17.2.4" - emittery@^0.7.1: version "0.7.1" resolved "https://registry.npmjs.org/emittery/-/emittery-0.7.1.tgz#c02375a927a40948c0345cc903072597f5270451" @@ -10844,65 +10978,70 @@ es6-weak-map@^2.0.3: es6-iterator "^2.0.3" es6-symbol "^3.1.1" -esbuild-android-arm64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.10.tgz#c854db57dc2d4df6f4f62185ca812f26a132bf1e" - integrity sha512-vzkTafHKoiMX4uIN1kBnE/HXYLpNT95EgGanVk6DHGeYgDolU0NBxjO7yZpq4ZGFPOx8384eAdDrBYhO11TAlQ== +esbuild-android-arm64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.21.tgz#8842d0c3b7c81fbe2dc46ddb416ffd6eb822184b" + integrity sha512-Bqgld1TY0wZv8TqiQmVxQFgYzz8ZmyzT7clXBDZFkOOdRybzsnj8AZuK1pwcLVA7Ya6XncHgJqIao7NFd3s0RQ== -esbuild-darwin-64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.10.tgz#c44fab6b8bfc83e5d083f513e4acbff14fb82eac" - integrity sha512-DJwzFVB95ZV7C3PQbf052WqaUuuMFXJeZJ0LKdnP1w+QOU0rlbKfX0tzuhoS//rOXUj1TFIwRuRsd0FX6skR7A== +esbuild-darwin-64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.21.tgz#ec7df02ad88ecf7f8fc23a3ed7917e07dea0c9c9" + integrity sha512-j+Eg+e13djzyYINVvAbOo2/zvZ2DivuJJTaBrJnJHSD7kUNuGHRkHoSfFjbI80KHkn091w350wdmXDNSgRjfYQ== -esbuild-darwin-arm64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.10.tgz#9454b3763b36407dc395c4c3529fb5ddd4a6225f" - integrity sha512-RNaaoZDg3nsqs5z56vYCjk/VJ76npf752W0rOaCl5lO5TsgV9zecfdYgt7dtUrIx8b7APhVaNYud+tGsDOVC9g== +esbuild-darwin-arm64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.21.tgz#0c2a977edec1ef54097ee56a911518c820d4e5e4" + integrity sha512-nDNTKWDPI0RuoPj5BhcSB2z5EmZJJAyRtZLIjyXSqSpAyoB8eyAKXl4lB8U2P78Fnh4Lh1le/fmpewXE04JhBQ== -esbuild-freebsd-64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.10.tgz#04eef46d5d5e4152c6b5a6a12f432db0fe7c89de" - integrity sha512-10B3AzW894u6bGZZhWiJOHw1uEHb4AFbUuBdyml1Ht0vIqd+KqWW+iY/yMwQAzILr2WJZqEhbOXRkJtY8aRqOw== +esbuild-freebsd-64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.21.tgz#f5b5fc1d031286c3a0949d1bda7db774b7d0404e" + integrity sha512-zIurkCHXhxELiDZtLGiexi8t8onQc2LtuE+S7457H/pP0g0MLRKMrsn/IN4LDkNe6lvBjuoZZi2OfelOHn831g== -esbuild-freebsd-arm64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.10.tgz#67ca88529543ada948737c95819253ead16494a7" - integrity sha512-mSQrKB7UaWvuryBTCo9leOfY2uEUSimAvcKIaUWbk5Hth9Sg+Try+qNA/NibPgs/vHkX0KFo/Rce6RPea+P15g== +esbuild-freebsd-arm64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.21.tgz#a05cab908013e4992b31a675850b8c44eb468c0c" + integrity sha512-wdxMmkJfbwcN+q85MpeUEamVZ40FNsBa9mPq8tAszDn8TRT2HoJvVRADPIIBa9SWWwlDChIMjkDKAnS3KS/sPA== -esbuild-linux-32@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.10.tgz#8f3d5fb0b9b616d6b604da781d71767d7679f64f" - integrity sha512-lktF09JgJLZ63ANYHIPdYe339PDuVn19Q/FcGKkXWf+jSPkn5xkYzAabboNGZNUgNqSJ/vY7VrOn6UrBbJjgYA== +esbuild-linux-32@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.21.tgz#638d244cc58b951f447addb4bade628d126ef84b" + integrity sha512-fmxvyzOPPh2xiEHojpCeIQP6pXcoKsWbz3ryDDIKLOsk4xp3GbpHIEAWP0xTeuhEbendmvBDVKbAVv3PnODXLg== -esbuild-linux-64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.10.tgz#c1c60a079c4709164bdd89fbb007a2edeea7c34a" - integrity sha512-K+gCQz2oLIIBI8ZM77e9sYD5/DwEpeYCrOQ2SYXx+R4OU2CT9QjJDi4/OpE7ko4AcYMlMW7qrOCuLSgAlEj4Wg== +esbuild-linux-64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.21.tgz#8eb634abee928be7e35b985fafbfef2f2e31397f" + integrity sha512-edZyNOv1ql+kpmlzdqzzDjRQYls+tSyi4QFi+PdBhATJFUqHsnNELWA9vMSzAaInPOEaVUTA5Ml28XFChcy4DA== -esbuild-linux-arm64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.10.tgz#d8f1f89190f6d8b6e06a1214aafba454e5daa990" - integrity sha512-+qocQuQvcp5wo/V+OLXxqHPc+gxHttJEvbU/xrCGE03vIMqraL4wMua8JQx0SWEnJCWP+Nhf//v8OSwz1Xr5kA== +esbuild-linux-arm64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.21.tgz#e05599ea6253b58394157da162d856f3ead62f9e" + integrity sha512-t5qxRkq4zdQC0zXpzSB2bTtfLgOvR0C6BXYaRE/6/k8/4SrkZcTZBeNu+xGvwCU4b5dU9ST9pwIWkK6T1grS8g== -esbuild-linux-arm@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.10.tgz#43192a00019a4553fb44e67f628fff0f560f16c2" - integrity sha512-BYa60dZ/KPmNKYxtHa3LSEdfKWHcm/RzP0MjB4AeBPhjS0D6/okhaBesZIY9kVIGDyeenKsJNOmnVt4+dhNnvQ== +esbuild-linux-arm@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.21.tgz#1ae1078231cf689d3ba894a32d3723c0be9b91fd" + integrity sha512-aSU5pUueK6afqmLQsbU+QcFBT62L+4G9hHMJDHWfxgid6hzhSmfRH9U/f+ymvxsSTr/HFRU4y7ox8ZyhlVl98w== -esbuild-linux-mips64le@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.10.tgz#f57bb8b2f1a3063cc91cfd787c8a9130cf863c16" - integrity sha512-nmUd2xoBXpGo4NJCEWoaBj+n4EtDoLEvEYc8Z3aSJrY0Oa6s04czD1flmhd0I/d6QEU8b7GQ9U0g/rtBfhtxBg== +esbuild-linux-mips64le@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.21.tgz#f05be62d126764e99b37edcac5bb49b78c7a8890" + integrity sha512-jLZLQGCNlUsmIHtGqNvBs3zN+7a4D9ckf0JZ+jQTwHdZJ1SgV9mAjbB980OFo66LoY+WeM7t3WEnq3FjI1zw4A== -esbuild-linux-ppc64le@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.10.tgz#becd965bfe3425d41e026f1c4678b393127fecbd" - integrity sha512-vsOWZjm0rZix7HSmqwPph9arRVCyPtUpcURdayQDuIhMG2/UxJxpbdRaa//w4zYqcJzAWwuyH2PAlyy0ZNuxqQ== +esbuild-linux-ppc64le@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.21.tgz#592c98d82dad7982268ef8deed858c4566f07ab1" + integrity sha512-4TWxpK391en2UBUw6GSrukToTDu6lL9vkm3Ll40HrI08WG3qcnJu7bl8e1+GzelDsiw1QmfAY/nNvJ6iaHRpCQ== -esbuild-linux-s390x@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.10.tgz#cc4228ac842febc48b84757814bed964a619be62" - integrity sha512-knArKKZm0ypIYWOWyOT7+accVwbVV1LZnl2FWWy05u9Tyv5oqJ2F5+X2Vqe/gqd61enJXQWqoufXopvG3zULOg== +esbuild-linux-riscv64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.21.tgz#0db7bd6f10d8f9afea973a7d6bf87b449b864b7b" + integrity sha512-fElngqOaOfTsF+u+oetDLHsPG74vB2ZaGZUqmGefAJn3a5z9Z2pNa4WpVbbKgHpaAAy5tWM1m1sbGohj6Ki6+Q== + +esbuild-linux-s390x@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.21.tgz#254a9354d34c9d1b41a3e21d2ec9269cbbb2c5df" + integrity sha512-brleZ6R5fYv0qQ7ZBwenQmP6i9TdvJCB092c/3D3pTLQHBGHJb5zWgKxOeS7bdHzmLy6a6W7GbFk6QKpjyD6QA== esbuild-loader@^2.18.0: version "2.18.0" @@ -10916,59 +11055,60 @@ esbuild-loader@^2.18.0: tapable "^2.2.0" webpack-sources "^2.2.0" -esbuild-netbsd-64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.10.tgz#6ec50d9e4547a7579f447307b19f66bbedfd868b" - integrity sha512-6Gg8neVcLeyq0yt9bZpReb8ntZ8LBEjthxrcYWVrBElcltnDjIy1hrzsujt0+sC2rL+TlSsE9dzgyuvlDdPp2w== +esbuild-netbsd-64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.21.tgz#4cb783d060b02bf3b897a9a12cce2b3b547726f8" + integrity sha512-nCEgsLCQ8RoFWVV8pVI+kX66ICwbPP/M9vEa0NJGIEB/Vs5sVGMqkf67oln90XNSkbc0bPBDuo4G6FxlF7PN8g== -esbuild-openbsd-64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.10.tgz#925ac3d2326cc219d514e1ca806e80e5143aa043" - integrity sha512-9rkHZzp10zI90CfKbFrwmQjqZaeDmyQ6s9/hvCwRkbOCHuto6RvMYH9ghQpcr5cUxD5OQIA+sHXi0zokRNXjcg== +esbuild-openbsd-64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.21.tgz#f886b93feefddbe573528fa4b421c9c6e2bc969b" + integrity sha512-h9zLMyVD0T73MDTVYIb/qUTokwI6EJH9O6wESuTNq6+XpMSr6C5aYZ4fvFKdNELW+Xsod+yDS2hV2JTUAbFrLA== -esbuild-sunos-64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.10.tgz#8d3576d8cac5c4f9f2a84be81b9078d424dbc739" - integrity sha512-mEU+pqkhkhbwpJj5DiN3vL0GUFR/yrL3qj8ER1amIVyRibKbj02VM1QaIuk1sy5DRVIKiFXXgCaHvH3RNWCHIw== +esbuild-sunos-64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.21.tgz#3829e4d57d4cb6950837fe90b0b67cdfb37cf13a" + integrity sha512-Kl+7Cot32qd9oqpLdB1tEGXEkjBlijrIxMJ0+vlDFaqsODutif25on0IZlFxEBtL2Gosd4p5WCV1U7UskNQfXA== -esbuild-windows-32@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.10.tgz#8a67fca4cb594a340566d66eef3f568f65057a48" - integrity sha512-Z5DieUL1N6s78dOSdL95KWf8Y89RtPGxIoMF+LEy8ChDsX+pZpz6uAVCn+YaWpqQXO+2TnrcbgBIoprq2Mco1g== +esbuild-windows-32@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.21.tgz#b858a22d1a82e53cdc59310cd56294133f7a95e7" + integrity sha512-V7vnTq67xPBUCk/9UtlolmQ798Ecjdr1ZoI1vcSgw7M82aSSt0eZdP6bh5KAFZU8pxDcx3qoHyWQfHYr11f22A== -esbuild-windows-64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.10.tgz#5e6d7c475ff6a71ad0aa4046894364e6c40a9249" - integrity sha512-LE5Mm62y0Bilu7RDryBhHIX8rK3at5VwJ6IGM3BsASidCfOBTzqcs7Yy0/Vkq39VKeTmy9/66BAfVoZRNznoDw== +esbuild-windows-64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.21.tgz#7bb5a027d5720cf9caf18a4bedd11327208f1f12" + integrity sha512-kDgHjKOHwjfJDCyRGELzVxiP/RBJBTA+wyspf78MTTJQkyPuxH2vChReNdWc+dU2S4gIZFHMdP1Qrl/k22ZmaA== -esbuild-windows-arm64@0.14.10: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.10.tgz#50ab9a83f6ccf71c272e58489ecc4d7375075f32" - integrity sha512-OJOyxDtabvcUYTc+O4dR0JMzLBz6G9+gXIHA7Oc5d5Fv1xiYa0nUeo8+W5s2e6ZkPRdIwOseYoL70rZz80S5BA== +esbuild-windows-arm64@0.14.21: + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.21.tgz#25df54521ad602c826b262ea2e7cc1fe80f5c2f5" + integrity sha512-8Sbo0zpzgwWrwjQYLmHF78f7E2xg5Ve63bjB2ng3V2aManilnnTGaliq2snYg+NOX60+hEvJHRdVnuIAHW0lVw== esbuild@^0.14.1, esbuild@^0.14.10, esbuild@^0.14.6: - version "0.14.10" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.10.tgz#10268d2b576b25ed6f8554553413988628a7767b" - integrity sha512-ibZb+NwFqBwHHJlpnFMtg4aNmVK+LUtYMFC9CuKs6lDCBEvCHpqCFZFEirpqt1jOugwKGx8gALNGvX56lQyfew== + version "0.14.21" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.21.tgz#b3e05f900f1c4394f596d60d63d9816468f0f671" + integrity sha512-7WEoNMBJdLN993dr9h0CpFHPRc3yFZD+EAVY9lg6syJJ12gc5fHq8d75QRExuhnMkT2DaRiIKFThRvDWP+fO+A== optionalDependencies: - esbuild-android-arm64 "0.14.10" - esbuild-darwin-64 "0.14.10" - esbuild-darwin-arm64 "0.14.10" - esbuild-freebsd-64 "0.14.10" - esbuild-freebsd-arm64 "0.14.10" - esbuild-linux-32 "0.14.10" - esbuild-linux-64 "0.14.10" - esbuild-linux-arm "0.14.10" - esbuild-linux-arm64 "0.14.10" - esbuild-linux-mips64le "0.14.10" - esbuild-linux-ppc64le "0.14.10" - esbuild-linux-s390x "0.14.10" - esbuild-netbsd-64 "0.14.10" - esbuild-openbsd-64 "0.14.10" - esbuild-sunos-64 "0.14.10" - esbuild-windows-32 "0.14.10" - esbuild-windows-64 "0.14.10" - esbuild-windows-arm64 "0.14.10" + esbuild-android-arm64 "0.14.21" + esbuild-darwin-64 "0.14.21" + esbuild-darwin-arm64 "0.14.21" + esbuild-freebsd-64 "0.14.21" + esbuild-freebsd-arm64 "0.14.21" + esbuild-linux-32 "0.14.21" + esbuild-linux-64 "0.14.21" + esbuild-linux-arm "0.14.21" + esbuild-linux-arm64 "0.14.21" + esbuild-linux-mips64le "0.14.21" + esbuild-linux-ppc64le "0.14.21" + esbuild-linux-riscv64 "0.14.21" + esbuild-linux-s390x "0.14.21" + esbuild-netbsd-64 "0.14.21" + esbuild-openbsd-64 "0.14.21" + esbuild-sunos-64 "0.14.21" + esbuild-windows-32 "0.14.21" + esbuild-windows-64 "0.14.21" + esbuild-windows-arm64 "0.14.21" escalade@^3.1.1: version "3.1.1" @@ -11378,7 +11518,7 @@ eventemitter3@^3.1.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== -eventemitter3@^4.0.0, eventemitter3@^4.0.4: +eventemitter3@^4.0.0, eventemitter3@^4.0.1, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -11401,53 +11541,55 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -"example-app@file:packages/app": - version "0.2.62" +"example-app@link:packages/app": + version "0.2.64" dependencies: - "@backstage/app-defaults" "^0.1.5" + "@backstage/app-defaults" "^0.1.7" "@backstage/catalog-model" "^0.9.10" - "@backstage/cli" "^0.13.0" - "@backstage/core-app-api" "^0.5.1" - "@backstage/core-components" "^0.8.6" + "@backstage/cli" "^0.13.2" + "@backstage/core-app-api" "^0.5.2" + "@backstage/core-components" "^0.8.8" "@backstage/core-plugin-api" "^0.6.0" - "@backstage/integration-react" "^0.1.19" - "@backstage/plugin-airbrake" "^0.1.1" - "@backstage/plugin-apache-airflow" "^0.1.4" - "@backstage/plugin-api-docs" "^0.7.0" - "@backstage/plugin-azure-devops" "^0.1.12" - "@backstage/plugin-badges" "^0.2.20" - "@backstage/plugin-catalog" "^0.7.10" - "@backstage/plugin-catalog-graph" "^0.2.8" - "@backstage/plugin-catalog-import" "^0.7.10" - "@backstage/plugin-catalog-react" "^0.6.12" - "@backstage/plugin-circleci" "^0.2.35" - "@backstage/plugin-cloudbuild" "^0.2.33" - "@backstage/plugin-code-coverage" "^0.1.23" - "@backstage/plugin-cost-insights" "^0.11.18" - "@backstage/plugin-explore" "^0.3.27" - "@backstage/plugin-gcp-projects" "^0.3.15" - "@backstage/plugin-github-actions" "^0.4.33" - "@backstage/plugin-gocd" "^0.1.2" - "@backstage/plugin-graphiql" "^0.2.28" - "@backstage/plugin-home" "^0.4.12" - "@backstage/plugin-jenkins" "^0.5.18" - "@backstage/plugin-kafka" "^0.2.26" - "@backstage/plugin-kubernetes" "^0.5.5" - "@backstage/plugin-lighthouse" "^0.2.35" - "@backstage/plugin-newrelic" "^0.3.14" - "@backstage/plugin-newrelic-dashboard" "^0.1.4" - "@backstage/plugin-org" "^0.4.0" - "@backstage/plugin-pagerduty" "0.3.23" - "@backstage/plugin-rollbar" "^0.3.24" - "@backstage/plugin-scaffolder" "^0.12.0" - "@backstage/plugin-search" "^0.6.0" - "@backstage/plugin-sentry" "^0.3.34" - "@backstage/plugin-shortcuts" "^0.1.20" - "@backstage/plugin-tech-insights" "^0.1.6" - "@backstage/plugin-tech-radar" "^0.5.3" - "@backstage/plugin-techdocs" "^0.13.1" - "@backstage/plugin-todo" "^0.1.20" - "@backstage/plugin-user-settings" "^0.3.17" + "@backstage/integration-react" "^0.1.21" + "@backstage/plugin-airbrake" "^0.1.3" + "@backstage/plugin-apache-airflow" "^0.1.6" + "@backstage/plugin-api-docs" "^0.7.2" + "@backstage/plugin-azure-devops" "^0.1.14" + "@backstage/plugin-badges" "^0.2.22" + "@backstage/plugin-catalog" "^0.7.12" + "@backstage/plugin-catalog-common" "^0.1.2" + "@backstage/plugin-catalog-graph" "^0.2.10" + "@backstage/plugin-catalog-import" "^0.8.1" + "@backstage/plugin-catalog-react" "^0.6.14" + "@backstage/plugin-circleci" "^0.2.37" + "@backstage/plugin-cloudbuild" "^0.2.35" + "@backstage/plugin-code-coverage" "^0.1.25" + "@backstage/plugin-cost-insights" "^0.11.20" + "@backstage/plugin-explore" "^0.3.29" + "@backstage/plugin-gcp-projects" "^0.3.17" + "@backstage/plugin-github-actions" "^0.4.35" + "@backstage/plugin-gocd" "^0.1.4" + "@backstage/plugin-graphiql" "^0.2.30" + "@backstage/plugin-home" "^0.4.14" + "@backstage/plugin-jenkins" "^0.5.20" + "@backstage/plugin-kafka" "^0.2.28" + "@backstage/plugin-kubernetes" "^0.5.7" + "@backstage/plugin-lighthouse" "^0.2.37" + "@backstage/plugin-newrelic" "^0.3.16" + "@backstage/plugin-newrelic-dashboard" "^0.1.6" + "@backstage/plugin-org" "^0.4.2-next.0" + "@backstage/plugin-pagerduty" "0.3.25" + "@backstage/plugin-permission-react" "^0.3.0" + "@backstage/plugin-rollbar" "^0.3.26" + "@backstage/plugin-scaffolder" "^0.12.2" + "@backstage/plugin-search" "^0.6.2" + "@backstage/plugin-sentry" "^0.3.36" + "@backstage/plugin-shortcuts" "^0.1.22" + "@backstage/plugin-tech-insights" "^0.1.8" + "@backstage/plugin-tech-radar" "^0.5.5" + "@backstage/plugin-techdocs" "^0.13.3" + "@backstage/plugin-todo" "^0.2.0" + "@backstage/plugin-user-settings" "^0.3.19" "@backstage/search-common" "^0.2.2" "@backstage/theme" "^0.2.14" "@material-ui/core" "^4.12.2" @@ -11518,19 +11660,6 @@ execa@5.1.1, execa@^5.0.0, execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -execa@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" - integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -11761,7 +11890,7 @@ extsprintf@^1.2.0: resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-decode-uri-component@^1.0.0: +fast-decode-uri-component@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== @@ -11771,6 +11900,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-equals@^2.0.0: + version "2.0.4" + resolved "https://registry.npmjs.org/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927" + integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w== + fast-glob@^3.1.1: version "3.2.2" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" @@ -11992,12 +12126,13 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-my-way@^2.2.2: - version "2.2.5" - resolved "https://registry.npmjs.org/find-my-way/-/find-my-way-2.2.5.tgz#86ce825266fa28cd962e538a45ec2aaa84c3d514" - integrity sha512-GjRZZlGcGmTh9t+6Xrj5K0YprpoAFCAiCPgmAH9Kb09O4oX6hYuckDfnDipYj+Q7B1GtYWSzDI5HEecNYscLQg== +find-my-way@^4.3.3: + version "4.5.1" + resolved "https://registry.npmjs.org/find-my-way/-/find-my-way-4.5.1.tgz#758e959194b90aea0270db18fff75e2fceb2239f" + integrity sha512-kE0u7sGoUFbMXcOG/xpkmz4sRLCklERnBcg7Ftuu1iAxsfEt2S46RLJ3Sq7vshsEy2wJT2hZxE58XZK27qa8kg== dependencies: - fast-decode-uri-component "^1.0.0" + fast-decode-uri-component "^1.0.1" + fast-deep-equal "^3.1.3" safe-regex2 "^2.0.0" semver-store "^0.3.0" @@ -12097,9 +12232,9 @@ fn.name@1.x.x: integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== follow-redirects@^1.0.0, follow-redirects@^1.14.0: - version "1.14.7" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685" - integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ== + version "1.14.8" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" + integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== for-in@^1.0.2: version "1.0.2" @@ -12136,22 +12271,21 @@ fork-ts-checker-webpack-plugin@^6.5.0: tapable "^1.0.0" fork-ts-checker-webpack-plugin@^7.0.0-alpha.8: - version "7.0.0-alpha.11" - resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.0.0-alpha.11.tgz#aa51ff15f203547ae6052b91fa912371db7557b9" - integrity sha512-10Q0sjG24BqIkAEFCb+JP0laM6gYO2+3ZV0lBHQ6kJ0+Ot2TffRFcyNkWQBRQqoyqtrDaHfxnjHJ+uXsMO13Bg== + version "7.1.1" + resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.1.1.tgz#9806c04f3aecaec8c9e8872806cd1f26d50d92a8" + integrity sha512-MgAv1g6+HLXz1iq2AZRfBjDhwBwk1HRSjuhNiMs8ax/3tsFo0rHedKwuE6ok44sJ5F5VgwPZae8gq0wiBAqYRw== dependencies: - "@babel/code-frame" "^7.14.5" + "@babel/code-frame" "^7.16.7" chalk "^4.1.2" - chokidar "^3.5.2" - cosmiconfig "^7.0.0" + chokidar "^3.5.3" + cosmiconfig "^7.0.1" deepmerge "^4.2.2" fs-extra "^10.0.0" - glob "^7.1.7" - memfs "^3.2.2" + memfs "^3.4.1" minimatch "^3.0.4" - schema-utils "3.1.1" + schema-utils "4.0.0" semver "^7.3.5" - tapable "^2.0.0" + tapable "^2.2.1" form-data-encoder@^1.4.3: version "1.6.0" @@ -12260,6 +12394,11 @@ from@~0: resolved "https://registry.npmjs.org/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= +fromentries@^1.3.1: + version "1.3.2" + resolved "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" + integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg== + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -12331,7 +12470,7 @@ fsevents@^2.1.2, fsevents@~2.3.2: resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -fstream@^1.0.0, fstream@^1.0.12: +fstream@^1.0.12: version "1.0.12" resolved "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== @@ -12351,6 +12490,36 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gauge@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/gauge/-/gauge-4.0.0.tgz#afba07aa0374a93c6219603b1fb83eaa2264d8f8" + integrity sha512-F8sU45yQpjQjxKkm1UOAhf0U/O0aFt//Fl7hsrNVto+patMHjs7dPI9mFOGUKbhrgKm0S3EjW3scMFuQmWSROw== + dependencies: + ansi-regex "^5.0.1" + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -12443,37 +12612,26 @@ get-monorepo-packages@^1.1.0: globby "^7.1.1" load-json-file "^4.0.0" -get-pkg-repo@^1.0.0: - version "1.4.0" - resolved "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" - integrity sha1-xztInAbYDMVTbCyFP54FIyBWly0= +get-pkg-repo@^4.0.0: + version "4.2.1" + resolved "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz#75973e1c8050c73f48190c52047c4cee3acbf385" + integrity sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA== dependencies: - hosted-git-info "^2.1.4" - meow "^3.3.0" - normalize-package-data "^2.3.0" - parse-github-repo-url "^1.3.0" + "@hutson/parse-repository-url" "^3.0.0" + hosted-git-info "^4.0.0" through2 "^2.0.0" + yargs "^16.2.0" get-port@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== -get-stdin@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" - integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= - get-stdin@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg== -get-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" - integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= - get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -12506,10 +12664,10 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= -getopts@2.2.5: - version "2.2.5" - resolved "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz#67a0fe471cacb9c687d817cab6450b96dde8313b" - integrity sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA== +getopts@2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz#71e5593284807e03e2427449d4f6712a268666f4" + integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA== getos@^3.2.1: version "3.2.1" @@ -12605,7 +12763,7 @@ glob@7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: +glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: version "7.2.0" resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -12617,13 +12775,6 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, gl once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" - integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A== - dependencies: - ini "^1.3.5" - global-dirs@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" @@ -12738,11 +12889,11 @@ google-gax@^2.12.0, google-gax@^2.24.1: retry-request "^4.0.0" google-p12-pem@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" - integrity sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA== + version "3.1.3" + resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz#5497998798ee86c2fc1f4bb1f92b7729baf37537" + integrity sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ== dependencies: - node-forge "^0.10.0" + node-forge "^1.0.0" got@^11.8.0, got@^11.8.2: version "11.8.2" @@ -12919,9 +13070,9 @@ graphql-sse@^1.0.1: integrity sha512-y2mVBN2KwNrzxX2KBncQ6kzc6JWvecxuBernrl0j65hsr6MAS3+Yn8PTFSOgRmtolxugepxveyZVQEuaNEbw3w== graphql-tag@^2.11.0: - version "2.12.4" - resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.4.tgz#d34066688a4f09e72d6f4663c74211e9b4b7c4bf" - integrity sha512-VV1U4O+9x99EkNpNmCUV5RZwq6MnK4+pGbRYWG+lA/m3uo7TSqJF81OkcOP148gFP6fzdl7JWYBrwWVTS9jXww== + version "2.12.6" + resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" + integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== dependencies: tslib "^2.1.0" @@ -12982,7 +13133,7 @@ handle-thing@^2.0.0: resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@^4.7.3, handlebars@^4.7.6: +handlebars@^4.7.3, handlebars@^4.7.6, handlebars@^4.7.7: version "4.7.7" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== @@ -13224,10 +13375,10 @@ hosted-git-info@^3.0.6: dependencies: lru-cache "^6.0.0" -hosted-git-info@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" - integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg== +hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: + version "4.1.0" + resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== dependencies: lru-cache "^6.0.0" @@ -13307,17 +13458,6 @@ http-deceiver@^1.2.7: resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= -http-errors@1.7.3: - version "1.7.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== - dependencies: - depd "~1.1.2" - inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - http-errors@1.8.1: version "1.8.1" resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c" @@ -13464,17 +13604,17 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -husky@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/husky/-/husky-6.0.0.tgz#810f11869adf51604c32ea577edbc377d7f9319e" - integrity sha512-SQS2gDTB7tBN486QSoKPKQItZw97BMOd+Kdb6ghfpBc0yXyzrddI0oDV5MkDAbuB4X2mO3/nj60TRMcYxwzZeQ== +husky@^7.0.4: + version "7.0.4" + resolved "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" + integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -13520,13 +13660,20 @@ ignore-by-default@^1.0.1: resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= -ignore-walk@^3.0.1, ignore-walk@^3.0.3: +ignore-walk@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== dependencies: minimatch "^3.0.4" +ignore-walk@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz#fc840e8346cf88a3a9380c5b17933cd8f4d39fa3" + integrity sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw== + dependencies: + minimatch "^3.0.4" + ignore@^3.3.5: version "3.3.10" resolved "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" @@ -13543,9 +13690,9 @@ ignore@^5.1.4, ignore@^5.1.8, ignore@^5.2.0: integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== immer@^9.0.1, immer@^9.0.7: - version "9.0.7" - resolved "https://registry.npmjs.org/immer/-/immer-9.0.7.tgz#b6156bd7db55db7abc73fd2fdadf4e579a701075" - integrity sha512-KGllzpbamZDvOIxnmJ0jI840g7Oikx58lBPWV0hUh7dtAyZpFqqrBZdKka5GlTwMTZ1Tjc/bKKW4VSFAt6BqMA== + version "9.0.12" + resolved "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20" + integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== immutable@^3.x.x: version "3.8.2" @@ -13614,13 +13761,6 @@ indefinite-observable@^2.0.1: dependencies: symbol-observable "1.2.0" -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - indent-string@^3.0.0: version "3.2.0" resolved "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" @@ -13700,7 +13840,7 @@ inline-style-prefixer@^6.0.0: dependencies: css-in-js-utils "^2.0.0" -inquirer@^7.0.4, inquirer@^7.3.3: +inquirer@^7.3.3: version "7.3.3" resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== @@ -13719,27 +13859,7 @@ inquirer@^7.0.4, inquirer@^7.3.3: strip-ansi "^6.0.0" through "^2.3.6" -inquirer@^8.0.0: - version "8.1.5" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.1.5.tgz#2dc5159203c826d654915b5fe6990fd17f54a150" - integrity sha512-G6/9xUqmt/r+UvufSyrPpt84NYwhKZ9jLsgMbQzlx804XErNupor8WQdBnBRrXmBfTPpuwf1sV+ss2ovjgdXIg== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.1" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.21" - mute-stream "0.0.8" - ora "^5.4.1" - run-async "^2.4.0" - rxjs "^7.2.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - -inquirer@^8.1.1, inquirer@^8.2.0: +inquirer@^8.0.0, inquirer@^8.1.1, inquirer@^8.2.0: version "8.2.0" resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ== @@ -13768,6 +13888,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internmap@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" + integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== + interpret@^1.0.0: version "1.4.0" resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -13778,10 +13903,10 @@ interpret@^2.2.0: resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -into-stream@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/into-stream/-/into-stream-5.1.1.tgz#f9a20a348a11f3c13face22763f2d02e127f4db8" - integrity sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA== +into-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz#4bfc1244c0128224e18b8870e85b2de8e66c6702" + integrity sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA== dependencies: from2 "^2.3.0" p-is-promise "^3.0.0" @@ -13932,12 +14057,12 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-ci@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994" - integrity sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ== +is-ci@^3.0.0, is-ci@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" + integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== dependencies: - ci-info "^3.1.1" + ci-info "^3.2.0" is-core-module@^2.1.0, is-core-module@^2.2.0: version "2.8.0" @@ -14030,11 +14155,6 @@ is-extglob@^2.1.1: resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-finite@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" - integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== - is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" @@ -14096,15 +14216,7 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3: resolved "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= -is-installed-globally@^0.3.1: - version "0.3.2" - resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" - integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== - dependencies: - global-dirs "^2.0.1" - is-path-inside "^3.0.1" - -is-installed-globally@~0.4.0: +is-installed-globally@^0.4.0, is-installed-globally@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== @@ -14144,10 +14256,10 @@ is-node-process@^1.0.1: resolved "https://registry.npmjs.org/is-node-process/-/is-node-process-1.0.1.tgz#4fc7ac3a91e8aac58175fe0578abbc56f2831b23" integrity sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ== -is-npm@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" - integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== +is-npm@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" + integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== is-number-object@^1.0.4: version "1.0.4" @@ -14188,11 +14300,6 @@ is-path-cwd@^2.2.0: resolved "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== -is-path-inside@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" - integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== - is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" @@ -14560,9 +14667,9 @@ istanbul-lib-source-maps@^4.0.0: source-map "^0.6.1" istanbul-reports@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" - integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== + version "3.1.4" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c" + integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" @@ -15517,15 +15624,15 @@ jsx-ast-utils@^3.2.1: array-includes "^3.1.3" object.assign "^4.1.2" -just-diff-apply@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-3.0.0.tgz#a77348d24f0694e378b57293dceb65bdf5a91c4f" - integrity sha512-K2MLc+ZC2DVxX4V61bIKPeMUUfj1YYZ3h0myhchDXOW1cKoPZMnjIoNCqv9bF2n5Oob1PFxuR2gVJxkxz4e58w== +just-diff-apply@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-4.0.1.tgz#da89c5a4ccb14aa8873c70e2c3b6695cef45dab5" + integrity sha512-AKOkzB5P6FkfP21UlZVX/OPXx/sC2GagpLX9cBxqHqDuRjwmZ/AJRKSNrB9jHPpRW1W1ONs6gly1gW46t055nQ== -just-diff@^3.0.1: - version "3.1.1" - resolved "https://registry.npmjs.org/just-diff/-/just-diff-3.1.1.tgz#d50c597c6fd4776495308c63bdee1b6839082647" - integrity sha512-sdMWKjRq8qWZEjDcVA6llnUT8RDEBIfOiGpYFPYa9u+2c39JCsejktSP7mj5eRid5EIvTzIpQ2kDOCw1Nq9BjQ== +just-diff@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/just-diff/-/just-diff-5.0.1.tgz#db8fe1cfeea1156f2374bfb289826dca28e7e390" + integrity sha512-X00TokkRIDotUIf3EV4xUm6ELc/IkqhS/vPSHdWnsM5y0HoNMfEqrazizI7g78lpHvnRSRt/PFfKtRqJCOGIuQ== just-extend@^4.0.2: version "4.2.1" @@ -15592,9 +15699,9 @@ keyv@^3.0.0: json-buffer "3.0.0" keyv@^4.0.0, keyv@^4.0.3: - version "4.0.5" - resolved "https://registry.npmjs.org/keyv/-/keyv-4.0.5.tgz#bb12b467aba372fab2a44d4420c00d3c4ebd484c" - integrity sha512-531pkGLqV3BMg0eDqqJFI0R1mkK1Nm5xIP2mM6keP5P8WfFtCkg2IOwplTUmlGoTgIg9yQYZ/kdihhz89XH3vA== + version "4.1.0" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.1.0.tgz#8ab5ca4ae6a34e05c629531d9a7f871575af0d5b" + integrity sha512-YsY3wr6HabE11/sscee+3nZ03XjvkrPWGouAmJFBdZoK92wiOlJCzI5/sDEIKdJhdhHO144ei45U9gXfbu14Uw== dependencies: json-buffer "3.0.1" @@ -15632,23 +15739,23 @@ kleur@^4.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== -knex@^0.95.1: - version "0.95.6" - resolved "https://registry.npmjs.org/knex/-/knex-0.95.6.tgz#5fc60ffc2935567bf122925526b1b06b8dbca785" - integrity sha512-noRcmkJl1MdicUbezrcr8OtVLcqQ/cfLIwgAx5EaxNxQOIJff88rBeyLywUScGhQNd/b78DIKKXZzLMrm6h/cw== +knex@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/knex/-/knex-1.0.2.tgz#1b79273f39f587a631c1a5515482c203d5971781" + integrity sha512-RuDKTylj6X/3nYomnsFV8sOdxTcehLHczOd3yrUdULE4pQR8jVlZxYt3vvIU04otJF0Cw9DCtRt05S4PN4kDpw== dependencies: - colorette "1.2.1" - commander "^7.1.0" - debug "4.3.1" + colorette "2.0.16" + commander "^8.3.0" + debug "4.3.3" escalade "^3.1.1" esm "^3.2.25" - getopts "2.2.5" + getopts "2.3.0" interpret "^2.2.0" lodash "^4.17.21" - pg-connection-string "2.4.0" - rechoir "^0.7.0" + pg-connection-string "2.5.0" + rechoir "^0.8.0" resolve-from "^5.0.0" - tarn "^3.0.1" + tarn "^3.0.2" tildify "2.0.0" kuler@^2.0.0: @@ -15668,7 +15775,7 @@ language-tags@^1.0.5: dependencies: language-subtag-registry "~0.3.2" -latest-version@5.1.0, latest-version@^5.0.0: +latest-version@5.1.0, latest-version@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== @@ -15826,9 +15933,9 @@ linkify-it@^3.0.1: uc.micro "^1.0.1" lint-staged@^12.2.0: - version "12.2.2" - resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-12.2.2.tgz#e03d93b41092316e0f38b37c9630da807aae3cca" - integrity sha512-bcHEoM1M/f+K1BYdHcEuIn8K+zMOSJR3mkny6PAuQiTgcSUcRbUWaUD6porAYypxF4k1vYZZ2HutZt1p94Z1jQ== + version "12.3.3" + resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-12.3.3.tgz#0a465962fe53baa2b4b9da50801ead49a910e03b" + integrity sha512-OqcLsqcPOqzvsfkxjeBpZylgJ3SRG1RYqc9LxC6tkt6tNsq1bNVkAixBwX09f6CobcHswzqVOCBpFR1Fck0+ag== dependencies: cli-truncate "^3.1.0" colorette "^2.0.16" @@ -15836,10 +15943,10 @@ lint-staged@^12.2.0: debug "^4.3.3" execa "^5.1.1" lilconfig "2.0.4" - listr2 "^3.13.5" + listr2 "^4.0.1" micromatch "^4.0.4" normalize-path "^3.0.0" - object-inspect "^1.11.1" + object-inspect "^1.12.0" string-argv "^0.3.1" supports-color "^9.2.1" yaml "^1.10.2" @@ -15878,17 +15985,17 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr2@^3.13.5: - version "3.14.0" - resolved "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" - integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== +listr2@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/listr2/-/listr2-4.0.2.tgz#04d66f8c8694a14920d7df08ebe01568948fb500" + integrity sha512-YcgwfCWpvPbj9FLUGqvdFvd3hrFWKpOeuXznRgfWEJ7RNr8b/IKKIKZABHx3aU+4CWN/iSAFFSReziQG6vTeIA== dependencies: cli-truncate "^2.1.0" colorette "^2.0.16" log-update "^4.0.0" p-map "^4.0.0" rfdc "^1.3.0" - rxjs "^7.5.1" + rxjs "^7.5.2" through "^2.3.8" wrap-ansi "^7.0.0" @@ -16240,17 +16347,6 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" -logform@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/logform/-/logform-2.3.0.tgz#a3997a05985de2ebd325ae0d166dffc9c6fe6b57" - integrity sha512-graeoWUH2knKbGthMtuG1EfaSPMZFZBIrhuJHhkS5ZseFBrc7DupCzihOQAzsK/qIKPQaPJ/lFQFctILUY5ARQ== - dependencies: - colors "^1.2.1" - fecha "^4.2.0" - ms "^2.1.1" - safe-stable-stringify "^1.1.0" - triple-beam "^1.3.0" - logform@^2.3.2: version "2.3.2" resolved "https://registry.npmjs.org/logform/-/logform-2.3.2.tgz#68babe6a74ab09a1fd15a9b1e6cbc7713d41cb5b" @@ -16284,14 +16380,6 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - lower-case-first@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/lower-case-first/-/lower-case-first-2.0.2.tgz#64c2324a2250bf7c37c5901e76a5b5309301160b" @@ -16346,6 +16434,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.3.1: + version "7.3.1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.3.1.tgz#7702e80694ec2bf19865567a469f2b081fcf53f5" + integrity sha512-nX1x4qUrKqwbIAhv4s9et4FIUVzNOpeY07bsjGUy8gwJrXH/wScImSQqXErmo/b2jZY2r0mohbLA9zVj7u1cNw== + lru-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -16412,6 +16505,28 @@ make-error@^1, make-error@^1.1.1, make-error@^1.3.6: resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +make-fetch-happen@^10.0.1: + version "10.0.2" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.0.2.tgz#0afb38d2f951b17ebc482b0b16c8d77f39dfe389" + integrity sha512-JSFLK53NJP22FL/eAGOyKsWbc2G3v+toPMD7Dq9PJKQCvK0i3t8hGkKxe+3YZzwYa+c0kxRHu7uxH3fvO+rsaA== + dependencies: + agentkeepalive "^4.2.0" + cacache "^15.3.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^7.3.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-fetch "^1.4.1" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + socks-proxy-agent "^6.1.1" + ssri "^8.0.1" + make-fetch-happen@^8.0.9: version "8.0.14" resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222" @@ -16433,7 +16548,7 @@ make-fetch-happen@^8.0.9: socks-proxy-agent "^5.0.0" ssri "^8.0.0" -make-fetch-happen@^9.0.1: +make-fetch-happen@^9.1.0: version "9.1.0" resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== @@ -16467,15 +16582,15 @@ map-cache@^0.2.0, map-cache@^0.2.2: resolved "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= -map-obj@^1.0.0, map-obj@^1.0.1: +map-obj@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= -map-obj@^4.0.0: - version "4.1.0" - resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" - integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g== +map-obj@^4.0.0, map-obj@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" + integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== map-stream@~0.1.0: version "0.1.0" @@ -16702,10 +16817,10 @@ media-typer@0.3.0: vinyl "^2.0.1" vinyl-file "^3.0.0" -memfs@^3.1.2, memfs@^3.2.2: - version "3.2.2" - resolved "https://registry.npmjs.org/memfs/-/memfs-3.2.2.tgz#5de461389d596e3f23d48bb7c2afb6161f4df40e" - integrity sha512-RE0CwmIM3CEvpcdK3rZ19BC4E6hv9kADkMN5rPduRak58cNArWLi/9jFLsa4rhsjfVxMP3v0jO7FHXq7SvFY5Q== +memfs@^3.1.2, memfs@^3.2.2, memfs@^3.4.1: + version "3.4.1" + resolved "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz#b78092f466a0dce054d63d39275b24c71d3f1305" + integrity sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw== dependencies: fs-monkey "1.0.3" @@ -16738,22 +16853,6 @@ memoizee@^0.4.15: next-tick "^1.1.0" timers-ext "^0.1.7" -meow@^3.3.0: - version "3.7.0" - resolved "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - meow@^6.0.0: version "6.1.1" resolved "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" @@ -17221,9 +17320,9 @@ min-indent@^1.0.0: integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY= mini-css-extract-plugin@^2.4.2: - version "2.5.2" - resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.2.tgz#b3b9b98320c2c054d92c16f6a94ddfdbbba13755" - integrity sha512-Lwgq9qLNyBK6yNLgzssXnq4r2+mB9Mz3cJWlM8kseysHIvTicFhDNimFgY94jjqlwhNzLPsq8wv4X+vOHtMdYA== + version "2.5.3" + resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.3.tgz#c5c79f9b22ce9b4f164e9492267358dbe35376d9" + integrity sha512-YseMB8cs8U/KCaAGQoqYmfUuhhGW0a9p9XvWXrxVOkE3/IiISTLw4ALNt7JR5B2eYauFM+PQGSbXMDmVbR7Tfw== dependencies: schema-utils "^4.0.0" @@ -17253,7 +17352,7 @@ minimist-options@4.1.0, minimist-options@^4.0.2: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -17272,10 +17371,10 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" -minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: - version "1.3.3" - resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" - integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== +minipass-fetch@^1.3.0, minipass-fetch@^1.3.2, minipass-fetch@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== dependencies: minipass "^3.1.0" minipass-sized "^1.0.3" @@ -17320,10 +17419,10 @@ minipass@^2.6.0, minipass@^2.9.0: safe-buffer "^5.1.2" yallist "^3.0.0" -minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: - version "3.1.3" - resolved "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" - integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3, minipass@^3.1.6: + version "3.1.6" + resolved "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== dependencies: yallist "^4.0.0" @@ -17334,7 +17433,7 @@ minizlib@^1.3.3: dependencies: minipass "^2.9.0" -minizlib@^2.0.0, minizlib@^2.1.1: +minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -17369,7 +17468,7 @@ mkdirp-infer-owner@^2.0.0: infer-owner "^1.0.4" mkdirp "^1.0.3" -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.1: +"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.5, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -17606,20 +17705,16 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -needle@^2.2.1: - version "2.6.0" - resolved "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe" - integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - negotiator@0.6.2, negotiator@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +negotiator@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -17664,10 +17759,10 @@ node-abort-controller@^3.0.1: resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== -node-addon-api@^3.0.0: - version "3.2.1" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" - integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-addon-api@^4.2.0: + version "4.3.0" + resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== node-cache@^5.1.2: version "5.1.2" @@ -17707,41 +17802,18 @@ node-fetch@2.6.5: dependencies: whatwg-url "^5.0.0" -node-fetch@2.6.7, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5: +node-fetch@2.6.7, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" -node-forge@^0.10.0: - version "0.10.0" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" - integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== - -node-forge@^1.2.0: +node-forge@^1.0.0, node-forge@^1.2.0: version "1.2.1" resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== -node-gyp@3.x: - version "3.8.0" - resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" - node-gyp@^5.0.2: version "5.1.0" resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.0.tgz#8e31260a7af4a2e2f994b0673d4e0b3866156332" @@ -17775,6 +17847,22 @@ node-gyp@^7.1.0: tar "^6.0.2" which "^2.0.2" +node-gyp@^8.2.0: + version "8.4.1" + resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^9.1.0" + nopt "^5.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -17831,49 +17919,26 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" -node-pre-gyp@^0.11.0: - version "0.11.0" - resolved "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" - integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - node-releases@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== nodemon@^2.0.2: - version "2.0.12" - resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.12.tgz#5dae4e162b617b91f1873b3bfea215dd71e144d5" - integrity sha512-egCTmNZdObdBxUBw6ZNwvZ/xzk24CKRs5K6d+5zbmrMr7rOpPmfPeF6OxM3DDpaRx331CQRFEktn+wrFFfBSOA== + version "2.0.15" + resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz#504516ce3b43d9dc9a955ccd9ec57550a31a8d4e" + integrity sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA== dependencies: - chokidar "^3.2.2" - debug "^3.2.6" + chokidar "^3.5.2" + debug "^3.2.7" ignore-by-default "^1.0.1" minimatch "^3.0.4" - pstree.remy "^1.1.7" + pstree.remy "^1.1.8" semver "^5.7.1" supports-color "^5.5.0" touch "^3.1.0" - undefsafe "^2.0.3" - update-notifier "^4.1.0" - -"nopt@2 || 3": - version "3.0.6" - resolved "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= - dependencies: - abbrev "1" + undefsafe "^2.0.5" + update-notifier "^5.1.0" nopt@^4.0.1: version "4.0.3" @@ -17897,7 +17962,7 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: +normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -17944,7 +18009,7 @@ normalize-url@^6.0.1: resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== -npm-bundled@^1.0.1, npm-bundled@^1.1.1: +npm-bundled@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== @@ -17995,15 +18060,6 @@ npm-package-arg@^8.1.2, npm-package-arg@^8.1.5: semver "^7.3.4" validate-npm-package-name "^3.0.0" -npm-packlist@^1.1.6: - version "1.4.8" - resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" - integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - npm-normalize-package-bin "^1.0.1" - npm-packlist@^2.1.4: version "2.1.4" resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.1.4.tgz#40e96b2b43787d0546a574542d01e066640d09da" @@ -18014,6 +18070,16 @@ npm-packlist@^2.1.4: npm-bundled "^1.1.1" npm-normalize-package-bin "^1.0.1" +npm-packlist@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-3.0.0.tgz#0370df5cfc2fcc8f79b8f42b37798dd9ee32c2a9" + integrity sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ== + dependencies: + glob "^7.1.6" + ignore-walk "^4.0.1" + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + npm-pick-manifest@^6.0.0: version "6.1.0" resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a" @@ -18033,17 +18099,17 @@ npm-pick-manifest@^6.1.0, npm-pick-manifest@^6.1.1: npm-package-arg "^8.1.2" semver "^7.3.4" -npm-registry-fetch@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz#68c1bb810c46542760d62a6a965f85a702d43a76" - integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== +npm-registry-fetch@^12.0.0, npm-registry-fetch@^12.0.1: + version "12.0.2" + resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-12.0.2.tgz#ae583bb3c902a60dae43675b5e33b5b1f6159f1e" + integrity sha512-Df5QT3RaJnXYuOwtXBXS9BWs+tHH2olvkCLh6jcR/b/u3DvPMlp3J0TvvYwplPKxHMOwfg287PYih9QqaVFoKA== dependencies: - make-fetch-happen "^9.0.1" - minipass "^3.1.3" - minipass-fetch "^1.3.0" + make-fetch-happen "^10.0.1" + minipass "^3.1.6" + minipass-fetch "^1.4.1" minipass-json-stream "^1.0.1" - minizlib "^2.0.0" - npm-package-arg "^8.0.0" + minizlib "^2.1.2" + npm-package-arg "^8.1.5" npm-registry-fetch@^9.0.0: version "9.0.0" @@ -18073,7 +18139,7 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2, npmlog@^4.1.2: +npmlog@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -18083,6 +18149,26 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: gauge "~2.7.3" set-blocking "~2.0.0" +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +npmlog@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/npmlog/-/npmlog-6.0.1.tgz#06f1344a174c06e8de9c6c70834cfba2964bba17" + integrity sha512-BTHDvY6nrRHuRfyjt1MAufLxYdVXZfd099H4+i1f0lPywNQyI4foeNXJRObB/uy+TYqUW0vAD9gbdSOXPst7Eg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.0" + set-blocking "^2.0.0" + nth-check@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" @@ -18150,7 +18236,7 @@ object-hash@^2.0.1, object-hash@^2.1.1, object-hash@^2.2.0: resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== -object-inspect@^1.11.0, object-inspect@^1.11.1, object-inspect@^1.9.0: +object-inspect@^1.11.0, object-inspect@^1.12.0, object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== @@ -18244,6 +18330,20 @@ octokit-plugin-create-pull-request@^3.10.0: dependencies: "@octokit/types" "^6.8.2" +octokit@^1.7.1: + version "1.7.1" + resolved "https://registry.npmjs.org/octokit/-/octokit-1.7.1.tgz#d86e51c8e0cec65cb64822ca2c9ff1b052593799" + integrity sha512-1b7eRgU8uWetHOWr8f9ptnVo2EKbrkOfocMeQdpgCt7tl/LK67HptFsy2Xg4fMjsJ/+onoBJW0hy/fO0In3/uA== + dependencies: + "@octokit/app" "^12.0.4" + "@octokit/core" "^3.5.1" + "@octokit/oauth-app" "^3.5.1" + "@octokit/plugin-paginate-rest" "^2.16.8" + "@octokit/plugin-rest-endpoint-methods" "^5.12.0" + "@octokit/plugin-retry" "^3.0.9" + "@octokit/plugin-throttling" "^3.5.1" + "@octokit/types" "^6.26.0" + oidc-token-hash@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz#ae6beec3ec20f0fd885e5400d175191d6e2f10c6" @@ -18393,7 +18493,7 @@ os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@0, osenv@^0.1.4: +osenv@^0.1.4: version "0.1.5" resolved "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== @@ -18547,6 +18647,14 @@ p-timeout@^3.2.0: dependencies: p-finally "^1.0.0" +p-transform@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/p-transform/-/p-transform-1.3.0.tgz#2da960ba92c6a56efbe75cbd1edf3ea7b3191049" + integrity sha512-UJKdSzgd3KOnXXAtqN5+/eeHcvTn1hBkesEmElVgvO/NAYcxAvmjzIGmnNd3Tb/gRAvMBdNRFD4qAWdHxY6QXg== + dependencies: + debug "^4.3.2" + p-queue "^6.6.2" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -18579,31 +18687,6 @@ packet-reader@1.0.0: resolved "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== -pacote@^11.1.11, pacote@^11.3.5: - version "11.3.5" - resolved "https://registry.npmjs.org/pacote/-/pacote-11.3.5.tgz#73cf1fc3772b533f575e39efa96c50be8c3dc9d2" - integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg== - dependencies: - "@npmcli/git" "^2.1.0" - "@npmcli/installed-package-contents" "^1.0.6" - "@npmcli/promise-spawn" "^1.2.0" - "@npmcli/run-script" "^1.8.2" - cacache "^15.0.5" - chownr "^2.0.0" - fs-minipass "^2.1.0" - infer-owner "^1.0.4" - minipass "^3.1.3" - mkdirp "^1.0.3" - npm-package-arg "^8.0.1" - npm-packlist "^2.1.4" - npm-pick-manifest "^6.0.0" - npm-registry-fetch "^11.0.0" - promise-retry "^2.0.1" - read-package-json-fast "^2.0.1" - rimraf "^3.0.2" - ssri "^8.0.1" - tar "^6.1.0" - pacote@^11.2.6: version "11.2.6" resolved "https://registry.npmjs.org/pacote/-/pacote-11.2.6.tgz#c0426e5d5c8d33aeea3461a75e1390f1ba78f953" @@ -18629,6 +18712,31 @@ pacote@^11.2.6: ssri "^8.0.1" tar "^6.1.0" +pacote@^12.0.0, pacote@^12.0.2: + version "12.0.3" + resolved "https://registry.npmjs.org/pacote/-/pacote-12.0.3.tgz#b6f25868deb810e7e0ddf001be88da2bcaca57c7" + integrity sha512-CdYEl03JDrRO3x18uHjBYA9TyoW8gy+ThVcypcDkxPtKlw76e4ejhYB6i9lJ+/cebbjpqPW/CijjqxwDTts8Ow== + dependencies: + "@npmcli/git" "^2.1.0" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^2.0.0" + cacache "^15.0.5" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.3" + mkdirp "^1.0.3" + npm-package-arg "^8.0.1" + npm-packlist "^3.0.0" + npm-pick-manifest "^6.0.0" + npm-registry-fetch "^12.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.1.0" + pako@^1.0.10, pako@~1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -18666,14 +18774,14 @@ parse-asn1@^5.0.0: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" -parse-conflict-json@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-1.1.1.tgz#54ec175bde0f2d70abf6be79e0e042290b86701b" - integrity sha512-4gySviBiW5TRl7XHvp1agcS7SOe0KZOjC//71dzZVWJrY9hCrgtvl5v3SyIxCZ4fZF47TxD9nfzmxcx76xmbUw== +parse-conflict-json@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-2.0.1.tgz#76647dd072e6068bcaff20be6ccea68a18e1fb58" + integrity sha512-Y7nYw+QaSGBto1LB9lgwOR05Rtz5SbuTf+Oe7HJ6SYQ/DHsvRjQ8O03oWdJbvkt6GzDWospgyZbGmjDYL0sDgA== dependencies: - json-parse-even-better-errors "^2.3.0" - just-diff "^3.0.1" - just-diff-apply "^3.0.0" + json-parse-even-better-errors "^2.3.1" + just-diff "^5.0.1" + just-diff-apply "^4.0.1" parse-entities@^2.0.0: version "2.0.0" @@ -18708,11 +18816,6 @@ parse-filepath@^1.0.2: map-cache "^0.2.0" path-root "^0.1.1" -parse-github-repo-url@^1.3.0: - version "1.4.1" - resolved "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" - integrity sha1-nn2LslKmy2ukJZUGC3v23z28H1A= - parse-json@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -18844,7 +18947,7 @@ passport-oauth2@1.2.0: passport-strategy "1.x.x" uid2 "0.0.x" -passport-oauth2@1.x.x, passport-oauth2@^1.1.2, passport-oauth2@^1.4.0, passport-oauth2@^1.5.0: +passport-oauth2@1.x.x, passport-oauth2@^1.1.2, passport-oauth2@^1.4.0, passport-oauth2@^1.6.1: version "1.6.1" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.6.1.tgz#c5aee8f849ce8bd436c7f81d904a3cd1666f181b" integrity sha512-ZbV43Hq9d/SBSYQ22GOiglFsjsD1YY/qdiptA+8ej+9C1dL1TVB+mBE5kDH/D4AJo50+2i8f4bx0vg4/yDDZCQ== @@ -18882,15 +18985,15 @@ passport-onelogin-oauth@^0.0.1: uid2 "0.0.3" passport-saml@^3.1.2: - version "3.2.0" - resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-3.2.0.tgz#72ec8203df6dd872a205b8d5f578859a4e723e42" - integrity sha512-EUzL+Wk8ZVdvOYhCBTkUrR1fwuMwF9za1FinFabP5Tl9qeJktsJWfoiBz7Fk6jQvpLwfnfryGdvwcOlGVct41A== + version "3.2.1" + resolved "https://registry.npmjs.org/passport-saml/-/passport-saml-3.2.1.tgz#c489a61a4c2dd93ddec1d53952a595b9f33e15e8" + integrity sha512-Y8aD94B6MTLht57BlBrDauEgvtWjuSeINKk7NadXlpT/OBmsoGGYPpb0FJeBtdyGX4GEbZARAkxvBEqsL8E7XQ== dependencies: "@xmldom/xmldom" "^0.7.5" debug "^4.3.2" passport-strategy "^1.0.0" xml-crypto "^2.1.3" - xml-encryption "^1.3.0" + xml-encryption "^2.0.0" xml2js "^0.4.23" xmlbuilder "^15.1.1" @@ -19055,12 +19158,7 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -pg-connection-string@2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10" - integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ== - -pg-connection-string@^2.3.0, pg-connection-string@^2.4.0, pg-connection-string@^2.5.0: +pg-connection-string@2.5.0, pg-connection-string@^2.3.0, pg-connection-string@^2.4.0, pg-connection-string@^2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== @@ -19070,10 +19168,10 @@ pg-int8@1.0.1: resolved "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== -pg-pool@^3.4.1: - version "3.4.1" - resolved "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz#0e71ce2c67b442a5e862a9c182172c37eda71e9c" - integrity sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ== +pg-pool@^3.5.1: + version "3.5.1" + resolved "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz#f499ce76f9bf5097488b3b83b19861f28e4ed905" + integrity sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ== pg-protocol@^1.5.0: version "1.5.0" @@ -19092,14 +19190,14 @@ pg-types@^2.1.0: postgres-interval "^1.1.0" pg@^8.3.0, pg@^8.4.0: - version "8.7.1" - resolved "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz#9ea9d1ec225980c36f94e181d009ab9f4ce4c471" - integrity sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA== + version "8.7.3" + resolved "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz#8a5bdd664ca4fda4db7997ec634c6e5455b27c44" + integrity sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw== dependencies: buffer-writer "2.0.0" packet-reader "1.0.0" pg-connection-string "^2.5.0" - pg-pool "^3.4.1" + pg-pool "^3.5.1" pg-protocol "^1.5.0" pg-types "^2.1.0" pgpass "1.x" @@ -19531,6 +19629,11 @@ postcss-unique-selectors@^5.0.2: alphanum-sort "^1.0.2" postcss-selector-parser "^6.0.5" +postcss-value-parser@^3.3.0: + version "3.3.1" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" @@ -19836,10 +19939,10 @@ psl@^1.1.28, psl@^1.1.33: resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== -pstree.remy@^1.1.7: - version "1.1.7" - resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz#c76963a28047ed61542dc361aa26ee55a7fa15f3" - integrity sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A== +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== public-encrypt@^4.0.0: version "4.0.3" @@ -19890,10 +19993,10 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pupa@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" - integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== +pupa@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== dependencies: escape-goat "^2.0.0" @@ -20034,7 +20137,7 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.2: +raw-body@2.4.2, raw-body@^2.4.1: version "2.4.2" resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ== @@ -20044,16 +20147,6 @@ raw-body@2.4.2: iconv-lite "0.4.24" unpipe "1.0.0" -raw-body@^2.4.1: - version "2.4.1" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" - integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== - dependencies: - bytes "3.1.0" - http-errors "1.7.3" - iconv-lite "0.4.24" - unpipe "1.0.0" - rc-progress@3.2.4: version "3.2.4" resolved "https://registry.npmjs.org/rc-progress/-/rc-progress-3.2.4.tgz#4036acdae2566438545bc4df2203248babaf7549" @@ -20072,7 +20165,7 @@ rc-util@^5.16.1: react-is "^16.12.0" shallowequal "^1.1.0" -rc@^1.2.7, rc@^1.2.8: +rc@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -20232,7 +20325,7 @@ react-inspector@^5.1.1: is-dom "^1.0.0" prop-types "^15.0.0" -react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.6, react-is@^16.9.0: +react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.6, react-is@^16.9.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -20289,6 +20382,15 @@ react-resize-detector@^2.3.0: prop-types "^15.6.0" resize-observer-polyfill "^1.5.0" +react-resize-detector@^6.6.3: + version "6.7.8" + resolved "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-6.7.8.tgz#318c85d1335e50f99d4fb8eb9ec34e066db597d0" + integrity sha512-0FaEcUBAbn+pq3PT5a9hHRebUfuS1SRLGLpIw8LydU7zX429I6XJgKerKAMPsJH0qWAl6o5bVKNqFJqr6tGPYw== + dependencies: + "@types/resize-observer-browser" "^0.1.6" + lodash "^4.17.21" + resize-observer-polyfill "^1.5.1" + react-router-dom@6.0.0-beta.0: version "6.0.0-beta.0" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.0-beta.0.tgz#9dcc8555365f22f7fbd09f26b6b82543f3eb97d6" @@ -20326,6 +20428,15 @@ react-smooth@^1.0.5: raf "^3.4.0" react-transition-group "^2.5.0" +react-smooth@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/react-smooth/-/react-smooth-2.0.0.tgz#561647b33e498b2e25f449b3c6689b2e9111bf91" + integrity sha512-wK4dBBR6P21otowgMT9toZk+GngMplGS1O5gk+2WSiHEXIrQgDvhR5IIlT74Vtu//qpTcipkgo21dD7a7AUNxw== + dependencies: + fast-equals "^2.0.0" + raf "^3.4.0" + react-transition-group "2.9.0" + react-sparklines@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/react-sparklines/-/react-sparklines-1.7.0.tgz#9b1d97e8c8610095eeb2ad658d2e1fcf91f91a60" @@ -20361,7 +20472,7 @@ react-text-truncate@^0.17.0: dependencies: prop-types "^15.5.7" -react-transition-group@^2.5.0: +react-transition-group@2.9.0, react-transition-group@^2.5.0: version "2.9.0" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d" integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg== @@ -20406,6 +20517,26 @@ react-use@^17.2.4: ts-easing "^0.2.0" tslib "^2.1.0" +react-use@^17.3.1: + version "17.3.2" + resolved "https://registry.npmjs.org/react-use/-/react-use-17.3.2.tgz#448abf515f47c41c32455024db28167cb6e53be8" + integrity sha512-bj7OD0/1wL03KyWmzFXAFe425zziuTf7q8olwCYBfOeFHY1qfO1FAMjROQLsLZYwG4Rx63xAfb7XAbBrJsZmEw== + dependencies: + "@types/js-cookie" "^2.2.6" + "@xobotyi/scrollbar-width" "^1.9.5" + copy-to-clipboard "^3.3.1" + fast-deep-equal "^3.1.3" + fast-shallow-equal "^1.0.0" + js-cookie "^2.2.1" + nano-css "^5.3.1" + react-universal-interface "^0.6.2" + resize-observer-polyfill "^1.5.1" + screenfull "^5.1.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^3.0.1" + ts-easing "^0.2.0" + tslib "^2.1.0" + react-virtualized-auto-sizer@^1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca" @@ -20558,7 +20689,7 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -20619,6 +20750,13 @@ recharts-scale@^0.4.2: dependencies: decimal.js-light "^2.4.1" +recharts-scale@^0.4.4: + version "0.4.5" + resolved "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" + integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== + dependencies: + decimal.js-light "^2.4.1" + recharts@^1.8.5: version "1.8.5" resolved "https://registry.npmjs.org/recharts/-/recharts-1.8.5.tgz#ca94a3395550946334a802e35004ceb2583fdb12" @@ -20636,6 +20774,26 @@ recharts@^1.8.5: recharts-scale "^0.4.2" reduce-css-calc "^1.3.0" +recharts@^2.1.5: + version "2.1.8" + resolved "https://registry.npmjs.org/recharts/-/recharts-2.1.8.tgz#ca8774fcec5f5d7ec15dedd638db9ee12faf1c09" + integrity sha512-Wi7ufdDGyvy/BPf1za1Ok7VeWB2KtEejaewO9ulmlUhvn5l5RPS4AOkrUfhtMRTTjgJ4K6AbWMDpwtDjczUHJA== + dependencies: + "@types/d3-interpolate" "^2.0.0" + "@types/d3-scale" "^3.0.0" + "@types/d3-shape" "^2.0.0" + classnames "^2.2.5" + d3-interpolate "^2.0.0" + d3-scale "^3.0.0" + d3-shape "^2.0.0" + eventemitter3 "^4.0.1" + lodash "^4.17.19" + react-is "^16.10.2" + react-resize-detector "^6.6.3" + react-smooth "^2.0.0" + recharts-scale "^0.4.4" + reduce-css-calc "^2.1.8" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -20643,12 +20801,12 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -rechoir@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz#32650fd52c21ab252aa5d65b19310441c7e03aca" - integrity sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q== +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== dependencies: - resolve "^1.9.0" + resolve "^1.20.0" recursive-readdir@^2.2.2: version "2.2.2" @@ -20657,14 +20815,6 @@ recursive-readdir@^2.2.2: dependencies: minimatch "3.0.4" -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - redent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -20682,6 +20832,14 @@ reduce-css-calc@^1.3.0: math-expression-evaluator "^1.2.14" reduce-function-call "^1.0.1" +reduce-css-calc@^2.1.8: + version "2.1.8" + resolved "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz#7ef8761a28d614980dc0c982f772c93f7a99de03" + integrity sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg== + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + reduce-function-call@^1.0.1: version "1.0.3" resolved "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz#60350f7fb252c0a67eb10fd4694d16909971300f" @@ -20947,26 +21105,19 @@ repeat-string@^1.5.2, repeat-string@^1.6.1: resolved "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -repeating@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" - integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= - dependencies: - is-finite "^1.0.0" - replace-ext@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== replace-in-file@^6.0.0: - version "6.1.0" - resolved "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.1.0.tgz#9f9ddd7bb70d6ad231d2ad692e1b646e73d06647" - integrity sha512-URzjyF3nucvejuY13HFd7O+Q6tFJRLKGHLYVvSh+LiZj3gFXzSYGnIkQflnJJulCAI2/RTZaZkpOtdVdW0EhQA== + version "6.3.2" + resolved "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.2.tgz#0f19835137177c89932f45df319f3539a019484f" + integrity sha512-Dbt5pXKvFVPL3WAaEB3ZX+95yP0CeAtIPJDwYzHbPP5EAHn+0UoegH/Wg3HKflU9dYBH8UnBC2NvY3P+9EZtTg== dependencies: - chalk "^4.0.0" - glob "^7.1.6" - yargs "^15.3.1" + chalk "^4.1.2" + glob "^7.2.0" + yargs "^17.2.1" replaceall@^0.1.6: version "0.1.6" @@ -20996,7 +21147,7 @@ request-promise-native@^1.0.8: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.87.0, request@^2.88.0, request@^2.88.2: +request@^2.88.0, request@^2.88.2: version "2.88.2" resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -21084,7 +21235,7 @@ resolve-url@^0.2.1: resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.9.0: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0: version "1.21.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f" integrity sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA== @@ -21206,7 +21357,7 @@ rifm@^0.7.0: dependencies: "@babel/runtime" "^7.3.1" -rimraf@2, rimraf@^2.6.1, rimraf@^2.6.3: +rimraf@2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -21335,14 +21486,7 @@ rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.3: dependencies: tslib "^1.9.0" -rxjs@^7.1.0, rxjs@^7.2.0: - version "7.3.0" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.3.0.tgz#39fe4f3461dc1e50be1475b2b85a0a88c1e938c6" - integrity sha512-p2yuGIg9S1epc3vrjKf6iVb3RCaAYjYskkO+jHIaV0IjOPlJop4UnodOoFb2xeNwlguqLYvGw1b1McillYb5Gw== - dependencies: - tslib "~2.1.0" - -rxjs@^7.5.1: +rxjs@^7.1.0, rxjs@^7.2.0, rxjs@^7.5.2: version "7.5.2" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.5.2.tgz#11e4a3a1dfad85dbf7fb6e33cbba17668497490b" integrity sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w== @@ -21390,7 +21534,7 @@ safe-stable-stringify@^1.1.0: resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz#c8a220ab525cd94e60ebf47ddc404d610dc5d84a" integrity sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw== -safe-stable-stringify@^2.2.0: +safe-stable-stringify@^2.2.0, safe-stable-stringify@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== @@ -21420,7 +21564,7 @@ sax@1.2.1: resolved "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= -sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: +sax@>=0.6.0, sax@~1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -21457,16 +21601,7 @@ schema-utils@2.7.0: ajv "^6.12.2" ajv-keywords "^3.4.1" -schema-utils@3.1.1, schema-utils@^3.1.0, schema-utils@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^4.0.0: +schema-utils@4.0.0, schema-utils@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== @@ -21476,6 +21611,15 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" +schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + scoped-regex@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/scoped-regex/-/scoped-regex-2.1.0.tgz#7b9be845d81fd9d21d1ec97c61a0b7cf86d2015f" @@ -21501,13 +21645,6 @@ select-hose@^2.0.0: resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -selfsigned@^1.10.7: - version "1.10.11" - resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz#24929cd906fe0f44b6d01fb23999a739537acbe9" - integrity sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA== - dependencies: - node-forge "^0.10.0" - selfsigned@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.0.tgz#e927cd5377cbb0a1075302cff8df1042cc2bce5b" @@ -21527,7 +21664,7 @@ semver-store@^0.3.0: resolved "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz#ce602ff07df37080ec9f4fb40b29576547befbe9" integrity sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg== -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -21549,11 +21686,6 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semve dependencies: lru-cache "^6.0.0" -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= - send@0.17.2: version "0.17.2" resolved "https://registry.npmjs.org/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" @@ -21673,11 +21805,6 @@ setprototypeof@1.1.0: resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -21732,7 +21859,7 @@ shell-quote@^1.7.3: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== -shelljs@^0.8.3, shelljs@^0.8.4, shelljs@^0.8.5: +shelljs@^0.8.4, shelljs@^0.8.5: version "0.8.5" resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== @@ -21763,10 +21890,10 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== signedsource@^1.0.0: version "1.0.0" @@ -21779,9 +21906,9 @@ simple-concat@^1.0.0: integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== simple-get@^3.0.2, simple-get@^3.0.3: - version "3.1.0" - resolved "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" - integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + version "3.1.1" + resolved "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" + integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== dependencies: decompress-response "^4.2.0" once "^1.3.1" @@ -21934,10 +22061,10 @@ socks-proxy-agent@^5.0.0: debug "4" socks "^2.3.3" -socks-proxy-agent@^6.0.0: - version "6.1.0" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.0.tgz#869cf2d7bd10fea96c7ad3111e81726855e285c3" - integrity sha512-57e7lwCN4Tzt3mXz25VxOErJKXlPfXmkMLnk310v/jwW20jWRVcgsOit+xNkN3eIEdB47GwnfAEBLacZ/wVIKg== +socks-proxy-agent@^6.0.0, socks-proxy-agent@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" + integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew== dependencies: agent-base "^6.0.2" debug "^4.3.1" @@ -22010,15 +22137,7 @@ source-map-resolve@^0.6.0: atob "^2.1.2" decode-uri-component "^0.2.0" -source-map-support@^0.5.16, source-map-support@^0.5.17, source-map-support@^0.5.6: - version "0.5.19" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@~0.5.20: +source-map-support@^0.5.16, source-map-support@^0.5.17, source-map-support@^0.5.6, source-map-support@~0.5.20: version "0.5.20" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== @@ -22178,16 +22297,6 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sqlite3@^5.0.1: - version "5.0.2" - resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.0.2.tgz#00924adcc001c17686e0a6643b6cbbc2d3965083" - integrity sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA== - dependencies: - node-addon-api "^3.0.0" - node-pre-gyp "^0.11.0" - optionalDependencies: - node-gyp "3.x" - sqlstring@^2.3.2: version "2.3.2" resolved "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz#cdae7169389a1375b18e885f2e60b3e460809514" @@ -22415,7 +22524,16 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -22432,24 +22550,6 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.0: version "5.1.0" resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.0.tgz#5ab00980cfb29f43e736b113a120a73a0fb569d3" @@ -22580,13 +22680,6 @@ strip-final-newline@^2.0.0: resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -22867,6 +22960,11 @@ swap-case@^2.0.2: dependencies: tslib "^2.0.3" +swr@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/swr/-/swr-1.1.2.tgz#9f3de2541931fccf03c48f322f1fc935a7551612" + integrity sha512-UsM0eo5T+kRPyWFZtWRx2XR5qzohs/LS4lDC0GCyLpCYFmsfTk28UCVDbOE9+KtoXY4FnwHYiF+ZYEU3hnJ1lQ== + symbol-observable@1.2.0, symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -22898,10 +22996,10 @@ tapable@^1.0.0: resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" - integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== +tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== tar-fs@2.1.1, tar-fs@^2.1.1: version "2.1.1" @@ -22934,16 +23032,7 @@ tar-stream@^2.0.0, tar-stream@^2.1.4, tar-stream@^2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^2.0.0: - version "2.2.2" - resolved "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" - integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== - dependencies: - block-stream "*" - fstream "^1.0.12" - inherits "2" - -tar@^4, tar@^4.4.12: +tar@^4.4.12: version "4.4.19" resolved "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== @@ -22968,10 +23057,10 @@ tar@^6.0.2, tar@^6.1.0, tar@^6.1.2: mkdirp "^1.0.3" yallist "^4.0.0" -tarn@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec" - integrity sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw== +tarn@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" + integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== tdigest@^0.1.1: version "0.1.1" @@ -22980,6 +23069,30 @@ tdigest@^0.1.1: dependencies: bintrees "1.0.1" +"techdocs-cli-embedded-app@link:packages/techdocs-cli-embedded-app": + version "0.2.63" + dependencies: + "@backstage/app-defaults" "^0.1.7" + "@backstage/catalog-model" "^0.9.10" + "@backstage/cli" "^0.13.2" + "@backstage/config" "^0.1.13" + "@backstage/core-app-api" "^0.5.2" + "@backstage/core-components" "^0.8.8" + "@backstage/core-plugin-api" "^0.6.0" + "@backstage/integration-react" "^0.1.21" + "@backstage/plugin-catalog" "^0.7.12" + "@backstage/plugin-techdocs" "^0.13.3" + "@backstage/test-utils" "^0.2.4" + "@backstage/theme" "^0.2.14" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + history "^5.0.0" + react "^17.0.2" + react-dom "^17.0.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^17.2.4" + teeny-request@^7.0.0: version "7.0.1" resolved "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.1.tgz#bdd41fdffea5f8fbc0d29392cb47bec4f66b2b4c" @@ -23014,13 +23127,6 @@ temp@^0.8.4: dependencies: rimraf "~2.6.2" -term-size@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" - integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= - dependencies: - execa "^0.7.0" - term-size@^2.1.0: version "2.2.0" resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" @@ -23290,11 +23396,6 @@ toggle-selection@^1.0.6: resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - toidentifier@1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" @@ -23390,11 +23491,6 @@ treeverse@^1.0.4: resolved "https://registry.npmjs.org/treeverse/-/treeverse-1.0.4.tgz#a6b0ebf98a1bca6846ddc7ecbc900df08cb9cd5f" integrity sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g== -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -23405,7 +23501,7 @@ trim-off-newlines@^1.0.0: resolved "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.3.tgz#8df24847fcb821b0ab27d58ab6efec9f2fe961a1" integrity sha512-kh6Tu6GbeSNMGfrrZh6Bb/4ZEHV1QlB4xNDBeog8Y9/QwFlKTRyWvY3Fs9tRDAMZliVUwieMgEdIeL/FtqjkJg== -triple-beam@^1.2.0, triple-beam@^1.3.0: +triple-beam@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== @@ -23436,9 +23532,9 @@ ts-log@^2.2.3: integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w== ts-node@^10.0.0, ts-node@^10.2.1, ts-node@^10.4.0: - version "10.4.0" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" - integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== + version "10.5.0" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.5.0.tgz#618bef5854c1fbbedf5e31465cbb224a1d524ef9" + integrity sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw== dependencies: "@cspotcode/source-map-support" "0.7.0" "@tsconfig/node10" "^1.0.7" @@ -23451,6 +23547,7 @@ ts-node@^10.0.0, ts-node@^10.2.1, ts-node@^10.4.0: create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" + v8-compile-cache-lib "^3.0.0" yn "3.1.1" ts-node@^9: @@ -23480,12 +23577,7 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@~2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" - integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== - -tslib@^2.3.0: +tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@~2.3.0: version "2.3.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -23600,7 +23692,7 @@ type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^1.2.2: +type-fest@^1.2.1, type-fest@^1.2.2: version "1.4.0" resolved "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== @@ -23663,9 +23755,9 @@ typescript@~4.4.4: integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA== typescript@~4.5.2, typescript@~4.5.4: - version "4.5.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" - integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== + version "4.5.5" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== ua-parser-js@^0.7.18: version "0.7.28" @@ -23737,12 +23829,10 @@ unc-path-regex@^0.1.2: resolved "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= -undefsafe@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" - integrity sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A== - dependencies: - debug "^2.2.0" +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== underscore@^1.12.1, underscore@^1.9.1: version "1.13.1" @@ -23975,22 +24065,23 @@ upath@^2.0.1: resolved "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== -update-notifier@^4.1.0: - version "4.1.3" - resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" - integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A== +update-notifier@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9" + integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw== dependencies: - boxen "^4.2.0" - chalk "^3.0.0" + boxen "^5.0.0" + chalk "^4.1.0" configstore "^5.0.1" has-yarn "^2.1.0" import-lazy "^2.1.0" is-ci "^2.0.0" - is-installed-globally "^0.3.1" - is-npm "^4.0.0" + is-installed-globally "^0.4.0" + is-npm "^5.0.0" is-yarn-global "^0.3.0" - latest-version "^5.0.0" - pupa "^2.0.1" + latest-version "^5.1.0" + pupa "^2.1.1" + semver "^7.3.4" semver-diff "^3.1.1" xdg-basedir "^4.0.0" @@ -24161,6 +24252,11 @@ uvu@^0.5.0: kleur "^4.0.3" sade "^1.7.3" +v8-compile-cache-lib@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" + integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== + v8-compile-cache@^2.0.3: version "2.1.0" resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" @@ -24307,10 +24403,13 @@ vm-browserify@^1.0.1: resolved "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -vm2@^3.9.5: - version "3.9.5" - resolved "https://registry.npmjs.org/vm2/-/vm2-3.9.5.tgz#5288044860b4bbace443101fcd3bddb2a0aa2496" - integrity sha512-LuCAHZN75H9tdrAiLFf030oW7nJV5xwNMuk1ymOZwopmuK3d2H4L1Kv4+GFHgarKiLfXXLFU+7LDABHnwOkWng== +vm2@^3.9.6: + version "3.9.7" + resolved "https://registry.npmjs.org/vm2/-/vm2-3.9.7.tgz#bb87aa677c97c61e23a6cb6547e44e990517a6f6" + integrity sha512-g/GZ7V0Mlmch3eDVOATvAXr1GsJNg6kQ5PjvYy3HbJMCRn5slNbo/u73Uy7r5yUej1cRa3ZjtoVwcWSQuQ/fow== + dependencies: + acorn "^8.7.0" + acorn-walk "^8.2.0" vscode-languageserver-types@^3.15.1: version "3.15.1" @@ -24608,7 +24707,7 @@ which-typed-array@^1.1.2: has-symbols "^1.0.1" is-typed-array "^1.1.3" -which@1, which@^1.2.9, which@^1.3.1: +which@^1.2.9, which@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -24622,19 +24721,12 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== +wide-align@^1.1.0, wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== dependencies: - string-width "^1.0.2 || 2" - -widest-line@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" - integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== - dependencies: - string-width "^2.1.1" + string-width "^1.0.2 || 2 || 3 || 4" widest-line@^3.1.0: version "3.1.0" @@ -24655,28 +24747,30 @@ windows-release@^3.1.0: dependencies: execa "^1.0.0" -winston-transport@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59" - integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw== +winston-transport@^4.4.2: + version "4.5.0" + resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" + integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== dependencies: - readable-stream "^2.3.7" - triple-beam "^1.2.0" + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" winston@^3.2.1: - version "3.3.3" - resolved "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170" - integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw== + version "3.5.1" + resolved "https://registry.npmjs.org/winston/-/winston-3.5.1.tgz#b25cc899d015836dbf8c583dec8c4c4483a0da2e" + integrity sha512-tbRtVy+vsSSCLcZq/8nXZaOie/S2tPXPFt4be/Q3vI/WtYwm7rrwidxVw2GRa38FIXcJ1kUM6MOZ9Jmnk3F3UA== dependencies: "@dabh/diagnostics" "^2.0.2" - async "^3.1.0" + async "^3.2.3" is-stream "^2.0.0" - logform "^2.2.0" + logform "^2.3.2" one-time "^1.0.0" readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" stack-trace "0.0.x" triple-beam "^1.3.0" - winston-transport "^4.4.0" + winston-transport "^4.4.2" word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" @@ -24746,6 +24840,14 @@ write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +write-file-atomic@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz#9faa33a964c1c85ff6f849b80b42a88c2c537c8f" + integrity sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + write-json-file@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz#65bbdc9ecd8a1458e15952770ccbadfcff5fe62a" @@ -24824,14 +24926,13 @@ xml-crypto@^2.1.3: "@xmldom/xmldom" "^0.7.0" xpath "0.0.32" -xml-encryption@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-1.3.0.tgz#4cad44a59bf8bdec76d7865ce0b89e13c09962f4" - integrity sha512-3P8C4egMMxSR1BmsRM+fG16a3WzOuUEQKS2U4c3AZ5v7OseIfdUeVkD8dwxIhuLryFZSRWUL5OP6oqkgU7hguA== +xml-encryption@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-2.0.0.tgz#d4e1eb3ec1f2c5d2a2a0a6e23d199237e8b4bf83" + integrity sha512-4Av83DdvAgUQQMfi/w8G01aJshbEZP9ewjmZMpS9t3H+OCZBDvyK4GJPnHGfWiXlArnPbYvR58JB9qF2x9Ds+Q== dependencies: "@xmldom/xmldom" "^0.7.0" escape-html "^1.0.3" - node-forge "^0.10.0" xpath "0.0.32" xml-name-validator@^3.0.0: @@ -24994,10 +25095,10 @@ yargs@^16.1.1, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0, yargs@^17.0.1, yargs@^17.1.1, yargs@^17.3.0: - version "17.3.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.3.0.tgz#295c4ffd0eef148ef3e48f7a2e0f58d0e4f26b1c" - integrity sha512-GQl1pWyDoGptFPJx9b9L6kmR33TGusZvXIZUT+BOz9f7X2L94oeAskFYLEg/FkhV06zZPBYLvLZRWeYId29lew== +yargs@^17.0.0, yargs@^17.0.1, yargs@^17.1.1, yargs@^17.2.1, yargs@^17.3.0: + version "17.3.1" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" + integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== dependencies: cliui "^7.0.2" escalade "^3.1.1" @@ -25044,13 +25145,13 @@ yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yeoman-environment@^3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-3.6.0.tgz#91267ced0d474c4dec330b83f232e72e796c0730" - integrity sha512-X16N9lhzRdUKFT8MZrpwjLDKsdgAUqh4VPR2wAXeAqjJJaUxYBxCQGFxtZVTf3vbyNuIHXPunwOLtK60bpapbg== +yeoman-environment@^3.9.1: + version "3.9.1" + resolved "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-3.9.1.tgz#21912bdee4b1d302a5c25a7d31338fa092ea7116" + integrity sha512-IdRnbQt/DSOSnao0oD9c+or1X2UrL+fx9eC0O7Lq/MGZV68nhv9k77MqG+hEAySPSlyCpocVlhfQwV62hczk5Q== dependencies: - "@npmcli/arborist" "^2.2.2" - are-we-there-yet "^1.1.5" + "@npmcli/arborist" "^4.0.4" + are-we-there-yet "^2.0.0" arrify "^2.0.1" binaryextensions "^4.15.0" chalk "^4.1.0" @@ -25072,9 +25173,10 @@ yeoman-environment@^3.6.0: mem-fs "^1.2.0 || ^2.0.0" mem-fs-editor "^8.1.2 || ^9.0.0" minimatch "^3.0.4" - npmlog "^4.1.2" + npmlog "^5.0.1" p-queue "^6.6.2" - pacote "^11.2.6" + p-transform "^1.3.0" + pacote "^12.0.2" preferred-pm "^3.0.3" pretty-bytes "^5.3.0" semver "^7.1.3"